├── lib ├── common │ ├── enum_state.dart │ ├── exception.dart │ ├── point_extension.dart │ ├── datetime_extension.dart │ ├── currency_rupiah_extension.dart │ ├── failure.dart │ ├── currency_input_formatter.dart │ ├── styles.dart │ └── routes.dart ├── domain │ ├── repositories │ │ ├── report_repository.dart │ │ ├── point_repository.dart │ │ ├── auth_repository.dart │ │ ├── user_repository.dart │ │ ├── cart_repository.dart │ │ ├── transaction_repository.dart │ │ ├── product_repository.dart │ │ └── category_repository.dart │ ├── usecases │ │ ├── auth │ │ │ ├── logout.dart │ │ │ ├── get_role.dart │ │ │ ├── set_role.dart │ │ │ ├── get_login_status.dart │ │ │ └── login_with_google.dart │ │ ├── cart │ │ │ ├── clear_cart.dart │ │ │ ├── get_all_carts_map.dart │ │ │ ├── add_product_to_cart.dart │ │ │ ├── add_product_quantity.dart │ │ │ └── reduce_product_quantity.dart │ │ ├── report │ │ │ ├── get_count_transactions_today.dart │ │ │ └── get_turn_over_transactions_today.dart │ │ ├── point │ │ │ ├── use_point.dart │ │ │ ├── save_point.dart │ │ │ └── get_all_points_history.dart │ │ ├── user │ │ │ ├── get_current_user.dart │ │ │ └── update_current_user.dart │ │ ├── product │ │ │ ├── get_all_products.dart │ │ │ ├── insert_product.dart │ │ │ ├── remove_product.dart │ │ │ └── update_product.dart │ │ ├── category │ │ │ ├── get_all_categories.dart │ │ │ ├── insert_category.dart │ │ │ ├── remove_category.dart │ │ │ └── update_category.dart │ │ └── transaction │ │ │ ├── get_all_transactions.dart │ │ │ ├── save_transaction.dart │ │ │ └── get_all_transactions_by_user_id.dart │ └── entity │ │ ├── category.dart │ │ ├── cart.dart │ │ ├── point.dart │ │ ├── user.dart │ │ ├── transaction.dart │ │ └── product.dart ├── presentation │ ├── blocs │ │ ├── point │ │ │ ├── point_event.dart │ │ │ ├── point_state.dart │ │ │ └── point_bloc.dart │ │ ├── report │ │ │ ├── report_event.dart │ │ │ ├── report_state.dart │ │ │ └── report_bloc.dart │ │ ├── auth │ │ │ ├── auth_event.dart │ │ │ ├── auth_state.dart │ │ │ └── auth_bloc.dart │ │ ├── profile │ │ │ ├── profile_event.dart │ │ │ ├── profile_state.dart │ │ │ └── profile_bloc.dart │ │ ├── transaction │ │ │ ├── transaction_event.dart │ │ │ ├── transaction_state.dart │ │ │ └── transaction_bloc.dart │ │ ├── product │ │ │ ├── product_event.dart │ │ │ ├── product_state.dart │ │ │ └── product_bloc.dart │ │ ├── category │ │ │ ├── category_event.dart │ │ │ ├── category_state.dart │ │ │ └── category_bloc.dart │ │ └── pos │ │ │ ├── pos_event.dart │ │ │ ├── pos_state.dart │ │ │ └── pos_bloc.dart │ ├── widgets │ │ ├── text_form_label.dart │ │ ├── warning_dialog.dart │ │ ├── error_dialog.dart │ │ ├── menu_card.dart │ │ ├── confirm_delete_dialog.dart │ │ └── action_dialog.dart │ └── pages │ │ ├── member │ │ ├── scan_qr_code_page.dart │ │ ├── member_transaction_page.dart │ │ ├── member_point_page.dart │ │ └── order_success_page.dart │ │ ├── splash_page.dart │ │ ├── login_page.dart │ │ ├── vendor │ │ ├── transaction_page.dart │ │ ├── product_page.dart │ │ └── pos_checkout_page.dart │ │ └── profile │ │ ├── profile_page.dart │ │ └── update_profile_page.dart ├── data │ ├── models │ │ ├── menu_model.dart │ │ ├── category_model.dart │ │ ├── product_cart_model.dart │ │ ├── cart_model.dart │ │ ├── point_model.dart │ │ ├── product_model.dart │ │ ├── user_model.dart │ │ └── transaction_model.dart │ ├── datasources │ │ ├── auth_local_data_source.dart │ │ ├── report_remote_data_source.dart │ │ ├── product_local_data_source.dart │ │ ├── category_local_data_source.dart │ │ ├── user_remote_data_source.dart │ │ ├── auth_remote_data_source.dart │ │ ├── point_remote_data_source.dart │ │ └── transaction_remote_data_source.dart │ ├── repositories │ │ ├── report_repository_impl.dart │ │ ├── user_repository_impl.dart │ │ ├── point_repository_impl.dart │ │ ├── transaction_repository_impl.dart │ │ ├── cart_repository_impl.dart │ │ ├── auth_repository_impl.dart │ │ ├── category_repository_impl.dart │ │ └── product_repository_impl.dart │ ├── pf │ │ └── preference_helper.dart │ └── db │ │ └── database_helper.dart └── main.dart ├── assets ├── logo │ ├── logo.png │ ├── background.png │ └── foreground.png ├── success_order.png ├── play_store_512.png ├── banner_promo │ ├── promo_1.png │ ├── promo_2.jpg │ ├── promo_3.jpg │ ├── promo_4.jpg │ └── promo_5.jpg └── screenshots │ ├── screenshot_1.png │ ├── screenshot_2.png │ ├── screenshot_3.png │ ├── screenshot_4.png │ ├── screenshot_5.png │ └── screenshot_6.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── triagung │ │ │ │ │ └── fic_mini_project │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── .gitignore ├── pubspec.yaml ├── analysis_options.yaml ├── .metadata └── README.md /lib/common/enum_state.dart: -------------------------------------------------------------------------------- 1 | enum PosActionState { noAction, success, failed } 2 | -------------------------------------------------------------------------------- /assets/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/logo/logo.png -------------------------------------------------------------------------------- /assets/success_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/success_order.png -------------------------------------------------------------------------------- /assets/logo/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/logo/background.png -------------------------------------------------------------------------------- /assets/logo/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/logo/foreground.png -------------------------------------------------------------------------------- /assets/play_store_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/play_store_512.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/banner_promo/promo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/banner_promo/promo_1.png -------------------------------------------------------------------------------- /assets/banner_promo/promo_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/banner_promo/promo_2.jpg -------------------------------------------------------------------------------- /assets/banner_promo/promo_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/banner_promo/promo_3.jpg -------------------------------------------------------------------------------- /assets/banner_promo/promo_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/banner_promo/promo_4.jpg -------------------------------------------------------------------------------- /assets/banner_promo/promo_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/banner_promo/promo_5.jpg -------------------------------------------------------------------------------- /assets/screenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/screenshots/screenshot_1.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/screenshots/screenshot_2.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/screenshots/screenshot_3.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/screenshots/screenshot_4.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/screenshots/screenshot_5.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/assets/screenshots/screenshot_6.png -------------------------------------------------------------------------------- /lib/common/exception.dart: -------------------------------------------------------------------------------- 1 | class DatabaseException implements Exception { 2 | final String message; 3 | 4 | DatabaseException(this.message); 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /lib/domain/repositories/report_repository.dart: -------------------------------------------------------------------------------- 1 | abstract class ReportRepository { 2 | Future getCountTransactionsToday(); 3 | Future getTurnOverTransactionsToday(); 4 | } 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/triagung128/fic-mini-project/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /lib/common/point_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension PointExtension on int { 4 | String get pointFormatter { 5 | return NumberFormat('#,##0', 'id_ID').format(this); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/triagung/fic_mini_project/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.triagung.fic_mini_project 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/datetime_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension DateTimeExtension on DateTime { 4 | String get dateTimeFormatter { 5 | return DateFormat('dd/MM/yyyy HH:mm').format(this); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /lib/presentation/blocs/point/point_event.dart: -------------------------------------------------------------------------------- 1 | part of 'point_bloc.dart'; 2 | 3 | abstract class PointEvent extends Equatable { 4 | const PointEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnFetchAllPointsHistory extends PointEvent {} 11 | -------------------------------------------------------------------------------- /lib/presentation/blocs/report/report_event.dart: -------------------------------------------------------------------------------- 1 | part of 'report_bloc.dart'; 2 | 3 | abstract class ReportEvent extends Equatable { 4 | const ReportEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnGetReportTransactionsToday extends ReportEvent {} 11 | -------------------------------------------------------------------------------- /lib/domain/usecases/auth/logout.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/auth_repository.dart'; 2 | 3 | class Logout { 4 | final AuthRepository repository; 5 | 6 | Logout(this.repository); 7 | 8 | Future execute() { 9 | return repository.logout(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/auth/get_role.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/auth_repository.dart'; 2 | 3 | class GetRole { 4 | final AuthRepository repository; 5 | 6 | GetRole(this.repository); 7 | 8 | Future execute() { 9 | return repository.getRole(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/cart/clear_cart.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/cart_repository.dart'; 2 | 3 | class ClearCart { 4 | final CartRepository repository; 5 | 6 | ClearCart(this.repository); 7 | 8 | Future execute() { 9 | return repository.clearCart(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/auth/set_role.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/auth_repository.dart'; 2 | 3 | class SetRole { 4 | final AuthRepository repository; 5 | 6 | SetRole(this.repository); 7 | 8 | Future execute(String role) { 9 | return repository.setRole(role); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/data/models/menu_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MenuModel { 4 | final Function() onPressed; 5 | final Widget icon; 6 | final String labelText; 7 | 8 | MenuModel({ 9 | required this.onPressed, 10 | required this.icon, 11 | required this.labelText, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/usecases/auth/get_login_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/auth_repository.dart'; 2 | 3 | class GetLoginStatus { 4 | final AuthRepository repository; 5 | 6 | GetLoginStatus(this.repository); 7 | 8 | bool execute() { 9 | return repository.getLoginStatus(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/domain/entity/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Category extends Equatable { 4 | final int? id; 5 | final String name; 6 | 7 | const Category({ 8 | required this.id, 9 | required this.name, 10 | }); 11 | 12 | @override 13 | List get props => [id, name]; 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/usecases/cart/get_all_carts_map.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/cart_repository.dart'; 2 | 3 | class GetAllCartsMap { 4 | final CartRepository repository; 5 | 6 | GetAllCartsMap(this.repository); 7 | 8 | Future> execute() { 9 | return repository.getAllCartsMap(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/presentation/blocs/auth/auth_event.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | abstract class AuthEvent extends Equatable { 4 | const AuthEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class LoginSubmitted extends AuthEvent {} 11 | 12 | class LoadLoginStatus extends AuthEvent {} 13 | 14 | class LogoutRequested extends AuthEvent {} 15 | -------------------------------------------------------------------------------- /lib/domain/usecases/report/get_count_transactions_today.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/report_repository.dart'; 2 | 3 | class GetCountTransactionsToday { 4 | final ReportRepository repository; 5 | 6 | GetCountTransactionsToday(this.repository); 7 | 8 | Future execute() { 9 | return repository.getCountTransactionsToday(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/report/get_turn_over_transactions_today.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/repositories/report_repository.dart'; 2 | 3 | class GetTurnOverTransactionsToday { 4 | final ReportRepository repository; 5 | 6 | GetTurnOverTransactionsToday(this.repository); 7 | 8 | Future execute() { 9 | return repository.getTurnOverTransactionsToday(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/entity/cart.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'package:fic_mini_project/domain/entity/product.dart'; 4 | 5 | class Cart extends Equatable { 6 | final List products; 7 | final int totalPrice; 8 | 9 | const Cart({ 10 | required this.products, 11 | this.totalPrice = 0, 12 | }); 13 | 14 | @override 15 | List get props => [products, totalPrice]; 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/repositories/point_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/point.dart'; 5 | 6 | abstract class PointRepository { 7 | Future>> getAllPointsHistory(); 8 | Future> savePoint(int point); 9 | Future> usePoint(int point); 10 | } 11 | -------------------------------------------------------------------------------- /lib/domain/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/user.dart'; 5 | 6 | abstract class AuthRepository { 7 | Future> loginWithGoogle(); 8 | bool getLoginStatus(); 9 | Future getRole(); 10 | Future setRole(String role); 11 | Future logout(); 12 | } 13 | -------------------------------------------------------------------------------- /lib/domain/repositories/user_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import 'package:fic_mini_project/common/failure.dart'; 5 | import 'package:fic_mini_project/domain/entity/user.dart'; 6 | 7 | abstract class UserRepository { 8 | Future> getCurrentUser(); 9 | Future> updateCurrentUser(User user, XFile? image); 10 | } 11 | -------------------------------------------------------------------------------- /lib/domain/usecases/point/use_point.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/repositories/point_repository.dart'; 5 | 6 | class UsePoint { 7 | final PointRepository repository; 8 | 9 | UsePoint(this.repository); 10 | 11 | Future> execute(int point) { 12 | return repository.usePoint(point); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/usecases/point/save_point.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/repositories/point_repository.dart'; 5 | 6 | class SavePoint { 7 | final PointRepository repository; 8 | 9 | SavePoint(this.repository); 10 | 11 | Future> execute(int point) { 12 | return repository.savePoint(point); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/repositories/cart_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/entity/cart.dart'; 2 | import 'package:fic_mini_project/domain/entity/product.dart'; 3 | 4 | abstract class CartRepository { 5 | Future addProductToCart(Product product); 6 | Future addProductQuantity(Product product); 7 | Future reduceProductQuantity(Product product); 8 | Future> getAllCartsMap(); 9 | Future clearCart(); 10 | } 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/domain/entity/point.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Point extends Equatable { 4 | final String userId; 5 | final int point; 6 | final bool isEntry; 7 | final DateTime createdAt; 8 | 9 | const Point({ 10 | required this.userId, 11 | required this.point, 12 | required this.isEntry, 13 | required this.createdAt, 14 | }); 15 | 16 | @override 17 | List get props => [userId, point, isEntry, createdAt]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/common/currency_rupiah_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension CurrencyRupiahStringExtension on int { 4 | String get intToFormatRupiah { 5 | return NumberFormat.currency( 6 | locale: 'id', 7 | symbol: 'Rp. ', 8 | decimalDigits: 0, 9 | ).format(this); 10 | } 11 | } 12 | 13 | extension CurrencyRupiahIntExtension on String { 14 | int get formatRupiahToInt { 15 | return int.parse(substring(4).replaceAll('.', '')); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/usecases/cart/add_product_to_cart.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/entity/cart.dart'; 2 | import 'package:fic_mini_project/domain/entity/product.dart'; 3 | import 'package:fic_mini_project/domain/repositories/cart_repository.dart'; 4 | 5 | class AddProductToCart { 6 | final CartRepository repository; 7 | 8 | AddProductToCart(this.repository); 9 | 10 | Future execute(Product product) { 11 | return repository.addProductToCart(product); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/usecases/auth/login_with_google.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/user.dart'; 5 | import 'package:fic_mini_project/domain/repositories/auth_repository.dart'; 6 | 7 | class Login { 8 | final AuthRepository repository; 9 | 10 | Login(this.repository); 11 | 12 | Future> execute() { 13 | return repository.loginWithGoogle(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/cart/add_product_quantity.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/entity/cart.dart'; 2 | import 'package:fic_mini_project/domain/entity/product.dart'; 3 | import 'package:fic_mini_project/domain/repositories/cart_repository.dart'; 4 | 5 | class AddProductQuantity { 6 | final CartRepository repository; 7 | 8 | AddProductQuantity(this.repository); 9 | 10 | Future execute(Product product) { 11 | return repository.addProductQuantity(product); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/usecases/cart/reduce_product_quantity.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/entity/cart.dart'; 2 | import 'package:fic_mini_project/domain/entity/product.dart'; 3 | import 'package:fic_mini_project/domain/repositories/cart_repository.dart'; 4 | 5 | class ReduceProductQuantity { 6 | final CartRepository repository; 7 | 8 | ReduceProductQuantity(this.repository); 9 | 10 | Future execute(Product product) { 11 | return repository.reduceProductQuantity(product); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/repositories/transaction_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 5 | 6 | abstract class TransactionRepository { 7 | Future>> getAllTransactions(); 8 | Future>> getAllTransactionsByUserId(); 9 | Future> saveTransaction(TransactionEntity transaction); 10 | } 11 | -------------------------------------------------------------------------------- /lib/domain/usecases/user/get_current_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/user.dart'; 5 | import 'package:fic_mini_project/domain/repositories/user_repository.dart'; 6 | 7 | class GetCurrentUser { 8 | final UserRepository repository; 9 | 10 | GetCurrentUser(this.repository); 11 | 12 | Future> execute() { 13 | return repository.getCurrentUser(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /lib/presentation/blocs/profile/profile_event.dart: -------------------------------------------------------------------------------- 1 | part of 'profile_bloc.dart'; 2 | 3 | abstract class ProfileEvent extends Equatable { 4 | const ProfileEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnFetchProfile extends ProfileEvent {} 11 | 12 | class OnUpdateProfile extends ProfileEvent { 13 | final User user; 14 | final XFile? image; 15 | 16 | const OnUpdateProfile(this.user, this.image); 17 | 18 | @override 19 | List get props => [user, image]; 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/domain/repositories/product_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | 6 | abstract class ProductRepository { 7 | Future>> getAllProducts(); 8 | Future> insertProduct(Product product); 9 | Future> updateProduct(Product product); 10 | Future> removeProduct(Product product); 11 | } 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/domain/usecases/product/get_all_products.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/domain/repositories/product_repository.dart'; 6 | 7 | class GetAllProducts { 8 | final ProductRepository repository; 9 | 10 | GetAllProducts(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getAllProducts(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/repositories/category_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/category.dart'; 5 | 6 | abstract class CategoryRepository { 7 | Future>> getAllCategories(); 8 | Future> insertCategory(Category category); 9 | Future> updateCategory(Category category); 10 | Future> removeCategory(Category category); 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/category/get_all_categories.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/category.dart'; 5 | import 'package:fic_mini_project/domain/repositories/category_repository.dart'; 6 | 7 | class GetAllCategories { 8 | final CategoryRepository repository; 9 | 10 | GetAllCategories(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getAllCategories(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/point/get_all_points_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/point.dart'; 5 | import 'package:fic_mini_project/domain/repositories/point_repository.dart'; 6 | 7 | class GetAllPointsHistory { 8 | final PointRepository repository; 9 | 10 | GetAllPointsHistory(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getAllPointsHistory(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/product/insert_product.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/domain/repositories/product_repository.dart'; 6 | 7 | class InsertProduct { 8 | final ProductRepository repository; 9 | 10 | InsertProduct(this.repository); 11 | 12 | Future> execute(Product product) { 13 | return repository.insertProduct(product); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/product/remove_product.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/domain/repositories/product_repository.dart'; 6 | 7 | class RemoveProduct { 8 | final ProductRepository repository; 9 | 10 | RemoveProduct(this.repository); 11 | 12 | Future> execute(Product product) { 13 | return repository.removeProduct(product); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/product/update_product.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/domain/repositories/product_repository.dart'; 6 | 7 | class UpdateProduct { 8 | final ProductRepository repository; 9 | 10 | UpdateProduct(this.repository); 11 | 12 | Future> execute(Product product) { 13 | return repository.updateProduct(product); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/category/insert_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/category.dart'; 5 | import 'package:fic_mini_project/domain/repositories/category_repository.dart'; 6 | 7 | class InsertCategory { 8 | final CategoryRepository repository; 9 | 10 | InsertCategory(this.repository); 11 | 12 | Future> execute(Category category) { 13 | return repository.insertCategory(category); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/category/remove_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/category.dart'; 5 | import 'package:fic_mini_project/domain/repositories/category_repository.dart'; 6 | 7 | class RemoveCategory { 8 | final CategoryRepository repository; 9 | 10 | RemoveCategory(this.repository); 11 | 12 | Future> execute(Category category) { 13 | return repository.removeCategory(category); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/category/update_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/category.dart'; 5 | import 'package:fic_mini_project/domain/repositories/category_repository.dart'; 6 | 7 | class UpdateCategory { 8 | final CategoryRepository repository; 9 | 10 | UpdateCategory(this.repository); 11 | 12 | Future> execute(Category category) { 13 | return repository.updateCategory(category); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/common/failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class Failure extends Equatable { 4 | final String message; 5 | 6 | const Failure(this.message); 7 | 8 | @override 9 | List get props => [message]; 10 | } 11 | 12 | class ServerFailure extends Failure { 13 | const ServerFailure(super.message); 14 | } 15 | 16 | class GoogleSignInFailure extends Failure { 17 | const GoogleSignInFailure(super.message); 18 | } 19 | 20 | class DatabaseFailure extends Failure { 21 | const DatabaseFailure(super.message); 22 | } 23 | -------------------------------------------------------------------------------- /lib/domain/usecases/transaction/get_all_transactions.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 5 | import 'package:fic_mini_project/domain/repositories/transaction_repository.dart'; 6 | 7 | class GetAllTransactions { 8 | final TransactionRepository repository; 9 | 10 | GetAllTransactions(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getAllTransactions(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/transaction/save_transaction.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 5 | import 'package:fic_mini_project/domain/repositories/transaction_repository.dart'; 6 | 7 | class SaveTransaction { 8 | final TransactionRepository repository; 9 | 10 | SaveTransaction(this.repository); 11 | 12 | Future> execute(TransactionEntity transaction) { 13 | return repository.saveTransaction(transaction); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/usecases/transaction/get_all_transactions_by_user_id.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 5 | import 'package:fic_mini_project/domain/repositories/transaction_repository.dart'; 6 | 7 | class GetAllTransactionsByUserId { 8 | final TransactionRepository repository; 9 | 10 | GetAllTransactionsByUserId(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getAllTransactionsByUserId(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/presentation/widgets/text_form_label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:fic_mini_project/common/styles.dart'; 4 | 5 | class TextFormLabel extends StatelessWidget { 6 | const TextFormLabel({ 7 | super.key, 8 | required this.label, 9 | }); 10 | 11 | final String label; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Text( 16 | label, 17 | style: Theme.of(context) 18 | .textTheme 19 | .bodyLarge! 20 | .copyWith(color: navyColor, fontWeight: FontWeight.w700), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/domain/usecases/user/update_current_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import 'package:fic_mini_project/common/failure.dart'; 5 | import 'package:fic_mini_project/domain/entity/user.dart'; 6 | import 'package:fic_mini_project/domain/repositories/user_repository.dart'; 7 | 8 | class UpdateCurrentUser { 9 | final UserRepository repository; 10 | 11 | UpdateCurrentUser(this.repository); 12 | 13 | Future> execute(User user, XFile? image) { 14 | return repository.updateCurrentUser(user, image); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/presentation/blocs/transaction/transaction_event.dart: -------------------------------------------------------------------------------- 1 | part of 'transaction_bloc.dart'; 2 | 3 | abstract class TransactionEvent extends Equatable { 4 | const TransactionEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnFetchAllTransactions extends TransactionEvent {} 11 | 12 | class OnFetchAllTransactionsByUserId extends TransactionEvent {} 13 | 14 | class OnSaveTransaction extends TransactionEvent { 15 | final TransactionEntity transaction; 16 | 17 | const OnSaveTransaction(this.transaction); 18 | 19 | @override 20 | List get props => [transaction]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/presentation/blocs/report/report_state.dart: -------------------------------------------------------------------------------- 1 | part of 'report_bloc.dart'; 2 | 3 | abstract class ReportState extends Equatable { 4 | const ReportState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ReportInitial extends ReportState {} 11 | 12 | class ReportLoading extends ReportState {} 13 | 14 | class ReportLoaded extends ReportState { 15 | final int countTransactions; 16 | final int turnOverTransactions; 17 | 18 | const ReportLoaded({ 19 | required this.countTransactions, 20 | required this.turnOverTransactions, 21 | }); 22 | 23 | @override 24 | List get props => [countTransactions, turnOverTransactions]; 25 | } 26 | -------------------------------------------------------------------------------- /lib/presentation/widgets/warning_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class WarningDialog extends StatelessWidget { 4 | const WarningDialog({ 5 | super.key, 6 | required this.title, 7 | required this.description, 8 | }); 9 | 10 | final String title; 11 | final String description; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return AlertDialog( 16 | title: Text(title), 17 | content: Text(description), 18 | actions: [ 19 | ElevatedButton( 20 | onPressed: () => Navigator.pop(context), 21 | child: const Text('Oke'), 22 | ), 23 | ], 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/domain/entity/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class User extends Equatable { 4 | final String id; 5 | final String? name; 6 | final String? email; 7 | final String? phoneNumber; 8 | final String? photoUrl; 9 | final String? role; 10 | final int? point; 11 | 12 | const User({ 13 | required this.id, 14 | this.name, 15 | this.email, 16 | this.phoneNumber, 17 | this.photoUrl, 18 | this.role, 19 | this.point, 20 | }); 21 | 22 | @override 23 | List get props => [ 24 | id, 25 | name, 26 | email, 27 | phoneNumber, 28 | photoUrl, 29 | role, 30 | point, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/blocs/point/point_state.dart: -------------------------------------------------------------------------------- 1 | part of 'point_bloc.dart'; 2 | 3 | abstract class PointState extends Equatable { 4 | const PointState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class PointInitial extends PointState {} 11 | 12 | class PointLoading extends PointState {} 13 | 14 | class AllPointsLoaded extends PointState { 15 | final List points; 16 | 17 | const AllPointsLoaded(this.points); 18 | 19 | @override 20 | List get props => [points]; 21 | } 22 | 23 | class PointFailure extends PointState { 24 | final String message; 25 | 26 | const PointFailure(this.message); 27 | 28 | @override 29 | List get props => [message]; 30 | } 31 | -------------------------------------------------------------------------------- /lib/presentation/widgets/error_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorDialog extends StatelessWidget { 4 | const ErrorDialog({ 5 | super.key, 6 | required this.message, 7 | }); 8 | 9 | final String message; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return AlertDialog( 14 | title: Text( 15 | 'Opss.. Error', 16 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 17 | color: Colors.red[400], 18 | ), 19 | ), 20 | content: Text(message), 21 | actions: [ 22 | ElevatedButton( 23 | onPressed: () => Navigator.pop(context), 24 | child: const Text('Oke'), 25 | ), 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/common/currency_input_formatter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'package:intl/intl.dart'; 4 | 5 | class CurrencyInputFormatter extends TextInputFormatter { 6 | @override 7 | TextEditingValue formatEditUpdate( 8 | TextEditingValue oldValue, 9 | TextEditingValue newValue, 10 | ) { 11 | if (newValue.selection.baseOffset == 0) return newValue; 12 | 13 | int value = int.parse(newValue.text); 14 | final formatter = NumberFormat.currency( 15 | locale: 'id', 16 | symbol: 'Rp. ', 17 | decimalDigits: 0, 18 | ); 19 | String newText = formatter.format(value); 20 | 21 | return newValue.copyWith( 22 | text: newText, 23 | selection: TextSelection.collapsed(offset: newText.length), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.13' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | tasks.register("clean", Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /lib/data/datasources/auth_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/data/pf/preference_helper.dart'; 2 | 3 | abstract class AuthLocalDataSource { 4 | Future getRole(); 5 | Future setRole(String role); 6 | Future removeRole(); 7 | } 8 | 9 | class AuthLocalDataSourceImpl extends AuthLocalDataSource { 10 | final PreferenceHelper preferencesHelper; 11 | 12 | AuthLocalDataSourceImpl(this.preferencesHelper); 13 | 14 | @override 15 | Future getRole() async { 16 | return await preferencesHelper.getRole(); 17 | } 18 | 19 | @override 20 | Future setRole(String role) async { 21 | await preferencesHelper.setRole(role); 22 | } 23 | 24 | @override 25 | Future removeRole() async { 26 | await preferencesHelper.removeRole(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/data/repositories/report_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/data/datasources/report_remote_data_source.dart'; 2 | import 'package:fic_mini_project/domain/repositories/report_repository.dart'; 3 | 4 | class ReportRepositoryImpl extends ReportRepository { 5 | final ReportRemoteDataSource remoteDataSource; 6 | 7 | ReportRepositoryImpl(this.remoteDataSource); 8 | 9 | @override 10 | Future getCountTransactionsToday() async { 11 | try { 12 | return await remoteDataSource.getCountTransactionsToday(); 13 | } catch (_) { 14 | return 0; 15 | } 16 | } 17 | 18 | @override 19 | Future getTurnOverTransactionsToday() async { 20 | try { 21 | return await remoteDataSource.getTurnOverTransactionsToday(); 22 | } catch (_) { 23 | return 0; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/domain/entity/transaction.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'package:fic_mini_project/domain/entity/cart.dart'; 4 | import 'package:fic_mini_project/domain/entity/user.dart'; 5 | 6 | class TransactionEntity extends Equatable { 7 | final User user; 8 | final Cart cart; 9 | final int usePoint; 10 | final int endTotalPrice; 11 | final String paymentMethod; 12 | final DateTime createdAt; 13 | 14 | const TransactionEntity({ 15 | required this.user, 16 | required this.cart, 17 | required this.usePoint, 18 | required this.endTotalPrice, 19 | required this.paymentMethod, 20 | required this.createdAt, 21 | }); 22 | 23 | @override 24 | List get props => [ 25 | user, 26 | cart, 27 | usePoint, 28 | endTotalPrice, 29 | paymentMethod, 30 | createdAt, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/blocs/point/point_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/domain/entity/point.dart'; 5 | import 'package:fic_mini_project/domain/usecases/point/get_all_points_history.dart'; 6 | 7 | part 'point_event.dart'; 8 | part 'point_state.dart'; 9 | 10 | class PointBloc extends Bloc { 11 | final GetAllPointsHistory getAllPointsHistory; 12 | 13 | PointBloc({required this.getAllPointsHistory}) : super(PointInitial()) { 14 | on((event, emit) async { 15 | emit(PointLoading()); 16 | 17 | final result = await getAllPointsHistory.execute(); 18 | 19 | result.fold( 20 | (failure) => emit(PointFailure(failure.message)), 21 | (data) => emit(AllPointsLoaded(data)), 22 | ); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/presentation/blocs/product/product_event.dart: -------------------------------------------------------------------------------- 1 | part of 'product_bloc.dart'; 2 | 3 | abstract class ProductEvent extends Equatable { 4 | const ProductEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnFetchAllProducts extends ProductEvent {} 11 | 12 | class OnCreateProduct extends ProductEvent { 13 | final Product product; 14 | 15 | const OnCreateProduct(this.product); 16 | 17 | @override 18 | List get props => [product]; 19 | } 20 | 21 | class OnUpdateProduct extends ProductEvent { 22 | final Product product; 23 | 24 | const OnUpdateProduct(this.product); 25 | 26 | @override 27 | List get props => [product]; 28 | } 29 | 30 | class OnDeleteProduct extends ProductEvent { 31 | final Product product; 32 | 33 | const OnDeleteProduct(this.product); 34 | 35 | @override 36 | List get props => [product]; 37 | } 38 | -------------------------------------------------------------------------------- /lib/presentation/blocs/category/category_event.dart: -------------------------------------------------------------------------------- 1 | part of 'category_bloc.dart'; 2 | 3 | abstract class CategoryEvent extends Equatable { 4 | const CategoryEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnFetchAllCategories extends CategoryEvent {} 11 | 12 | class OnCreateCategory extends CategoryEvent { 13 | final Category category; 14 | 15 | const OnCreateCategory(this.category); 16 | 17 | @override 18 | List get props => [category]; 19 | } 20 | 21 | class OnUpdateCategory extends CategoryEvent { 22 | final Category category; 23 | 24 | const OnUpdateCategory(this.category); 25 | 26 | @override 27 | List get props => [category]; 28 | } 29 | 30 | class OnDeleteCategory extends CategoryEvent { 31 | final Category category; 32 | 33 | const OnDeleteCategory(this.category); 34 | 35 | @override 36 | List get props => [category]; 37 | } 38 | -------------------------------------------------------------------------------- /lib/presentation/blocs/pos/pos_event.dart: -------------------------------------------------------------------------------- 1 | part of 'pos_bloc.dart'; 2 | 3 | abstract class PosEvent extends Equatable { 4 | const PosEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnClearCart extends PosEvent {} 11 | 12 | class OnAddProductToCart extends PosEvent { 13 | final Product product; 14 | 15 | const OnAddProductToCart(this.product); 16 | 17 | @override 18 | List get props => [product]; 19 | } 20 | 21 | class OnAddProductQuantity extends PosEvent { 22 | final Product product; 23 | 24 | const OnAddProductQuantity(this.product); 25 | 26 | @override 27 | List get props => [product]; 28 | } 29 | 30 | class OnReduceProductQuantity extends PosEvent { 31 | final Product product; 32 | 33 | const OnReduceProductQuantity(this.product); 34 | 35 | @override 36 | List get props => [product]; 37 | } 38 | 39 | class OnPosAction extends PosEvent {} 40 | -------------------------------------------------------------------------------- /lib/presentation/blocs/auth/auth_state.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | abstract class AuthState extends Equatable { 4 | const AuthState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class AuthInitial extends AuthState {} 11 | 12 | class AuthLoading extends AuthState {} 13 | 14 | class AuthFailure extends AuthState { 15 | final String message; 16 | 17 | const AuthFailure(this.message); 18 | 19 | @override 20 | List get props => [message]; 21 | } 22 | 23 | class LoginAsVendorAuthenticated extends AuthState {} 24 | 25 | class LoginAsMemberAuthenticated extends AuthState {} 26 | 27 | class LoginStatusLoaded extends AuthState { 28 | final bool status; 29 | final String role; 30 | 31 | const LoginStatusLoaded({required this.status, required this.role}); 32 | 33 | @override 34 | List get props => [status, role]; 35 | } 36 | 37 | class LogoutSuccess extends AuthState {} 38 | -------------------------------------------------------------------------------- /lib/data/models/category_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'package:fic_mini_project/domain/entity/category.dart'; 4 | 5 | class CategoryModel extends Equatable { 6 | final int? id; 7 | final String name; 8 | 9 | const CategoryModel({ 10 | required this.id, 11 | required this.name, 12 | }); 13 | 14 | Map toMap() => { 15 | 'id': id, 16 | 'name': name, 17 | }; 18 | 19 | factory CategoryModel.fromMap(Map map) => CategoryModel( 20 | id: map['id'], 21 | name: map['name'], 22 | ); 23 | 24 | factory CategoryModel.fromEntity(Category category) => CategoryModel( 25 | id: category.id, 26 | name: category.name, 27 | ); 28 | 29 | Category toEntity() => Category( 30 | id: id, 31 | name: name, 32 | ); 33 | 34 | @override 35 | List get props => [id, name]; 36 | } 37 | -------------------------------------------------------------------------------- /lib/presentation/blocs/pos/pos_state.dart: -------------------------------------------------------------------------------- 1 | part of 'pos_bloc.dart'; 2 | 3 | class PosState extends Equatable { 4 | final Cart cart; 5 | final PosActionState actionState; 6 | final Map cartMap; 7 | 8 | const PosState({ 9 | required this.cart, 10 | required this.actionState, 11 | required this.cartMap, 12 | }); 13 | 14 | @override 15 | List get props => [cart, actionState, cartMap]; 16 | 17 | factory PosState.initial() { 18 | return const PosState( 19 | cart: Cart(products: []), 20 | actionState: PosActionState.noAction, 21 | cartMap: {}, 22 | ); 23 | } 24 | 25 | PosState copyWith({ 26 | Cart? cart, 27 | PosActionState? actionState, 28 | Map? cartMap, 29 | }) { 30 | return PosState( 31 | cart: cart ?? this.cart, 32 | actionState: actionState ?? this.actionState, 33 | cartMap: cartMap ?? this.cartMap, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/presentation/blocs/transaction/transaction_state.dart: -------------------------------------------------------------------------------- 1 | part of 'transaction_bloc.dart'; 2 | 3 | abstract class TransactionState extends Equatable { 4 | const TransactionState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class TransactionInitial extends TransactionState {} 11 | 12 | class TransactionLoading extends TransactionState {} 13 | 14 | class TransactionEmpty extends TransactionState {} 15 | 16 | class TransactionFailure extends TransactionState { 17 | final String message; 18 | 19 | const TransactionFailure(this.message); 20 | 21 | @override 22 | List get props => [message]; 23 | } 24 | 25 | class TransactionSuccess extends TransactionState {} 26 | 27 | class AllTransactionsLoaded extends TransactionState { 28 | final List transactions; 29 | 30 | const AllTransactionsLoaded(this.transactions); 31 | 32 | @override 33 | List get props => [transactions]; 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | /lib/firebase_options.dart 47 | 48 | /assets/screenshots_app -------------------------------------------------------------------------------- /lib/domain/entity/product.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | 5 | import 'package:fic_mini_project/domain/entity/category.dart'; 6 | 7 | class Product extends Equatable { 8 | final int? id; 9 | final String name; 10 | final int price; 11 | final Category? category; 12 | final Uint8List? image; 13 | final int quantity; 14 | 15 | const Product({ 16 | required this.id, 17 | required this.name, 18 | required this.price, 19 | required this.category, 20 | required this.image, 21 | this.quantity = 0, 22 | }); 23 | 24 | const Product.cart({ 25 | required this.id, 26 | required this.name, 27 | required this.price, 28 | required this.quantity, 29 | this.category, 30 | this.image, 31 | }); 32 | 33 | @override 34 | List get props => [ 35 | id, 36 | name, 37 | price, 38 | category, 39 | image, 40 | quantity, 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /lib/data/pf/preference_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class PreferenceHelper { 4 | static PreferenceHelper? _preferenceHelper; 5 | 6 | PreferenceHelper._instance() { 7 | _preferenceHelper = this; 8 | } 9 | 10 | factory PreferenceHelper() => 11 | _preferenceHelper ?? PreferenceHelper._instance(); 12 | 13 | static SharedPreferences? _sharedPreferences; 14 | 15 | Future get sharedPreferences async { 16 | return _sharedPreferences ??= await SharedPreferences.getInstance(); 17 | } 18 | 19 | static const _roleKey = 'ROLE'; 20 | 21 | Future getRole() async { 22 | final prefs = await sharedPreferences; 23 | return prefs.getString(_roleKey); 24 | } 25 | 26 | Future setRole(String role) async { 27 | final prefs = await sharedPreferences; 28 | prefs.remove(_roleKey); 29 | prefs.setString(_roleKey, role); 30 | } 31 | 32 | Future removeRole() async { 33 | final prefs = await sharedPreferences; 34 | prefs.remove(_roleKey); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/presentation/blocs/profile/profile_state.dart: -------------------------------------------------------------------------------- 1 | part of 'profile_bloc.dart'; 2 | 3 | abstract class ProfileState extends Equatable { 4 | const ProfileState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ProfileInitial extends ProfileState {} 11 | 12 | class ProfileLoading extends ProfileState {} 13 | 14 | class ProfileFailure extends ProfileState { 15 | final String message; 16 | 17 | const ProfileFailure(this.message); 18 | 19 | @override 20 | List get props => [message]; 21 | } 22 | 23 | class ProfileLoaded extends ProfileState { 24 | final User user; 25 | 26 | const ProfileLoaded(this.user); 27 | 28 | @override 29 | List get props => [user]; 30 | } 31 | 32 | class ProfileUpdateFailure extends ProfileState { 33 | final String message; 34 | 35 | const ProfileUpdateFailure(this.message); 36 | 37 | @override 38 | List get props => [message]; 39 | } 40 | 41 | class ProfileUpdateSuccess extends ProfileState { 42 | final String message; 43 | 44 | const ProfileUpdateSuccess(this.message); 45 | 46 | @override 47 | List get props => [message]; 48 | } 49 | -------------------------------------------------------------------------------- /lib/data/models/product_cart_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/domain/entity/product.dart'; 2 | 3 | class ProductCartModel { 4 | final int? id; 5 | final String name; 6 | final int price; 7 | int quantity; 8 | 9 | ProductCartModel({ 10 | required this.id, 11 | required this.name, 12 | required this.price, 13 | this.quantity = 0, 14 | }); 15 | 16 | factory ProductCartModel.fromMap(Map map) => 17 | ProductCartModel( 18 | id: map['id'], 19 | name: map['name'], 20 | price: map['price'], 21 | quantity: map['quantity'], 22 | ); 23 | 24 | factory ProductCartModel.fromEntity(Product product) => ProductCartModel( 25 | id: product.id, 26 | name: product.name, 27 | price: product.price, 28 | quantity: product.quantity, 29 | ); 30 | 31 | Map toMap() => { 32 | 'id': id, 33 | 'name': name, 34 | 'price': price, 35 | 'quantity': quantity, 36 | }; 37 | 38 | Product toEntity() => Product.cart( 39 | id: id, 40 | name: name, 41 | price: price, 42 | quantity: quantity, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/data/models/cart_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/data/models/product_cart_model.dart'; 2 | import 'package:fic_mini_project/domain/entity/cart.dart'; 3 | 4 | class CartModel { 5 | final List products; 6 | int totalPrice; 7 | 8 | CartModel({ 9 | required this.products, 10 | this.totalPrice = 0, 11 | }); 12 | 13 | Cart toEntity() => Cart( 14 | products: products.map((item) => item.toEntity()).toList(), 15 | totalPrice: totalPrice, 16 | ); 17 | 18 | Map toMap() => { 19 | 'products': products.map((item) => item.toMap()).toList(), 20 | 'total_price': totalPrice, 21 | }; 22 | 23 | factory CartModel.fromMap(Map map) => CartModel( 24 | products: List.from( 25 | map['products'].map((product) => ProductCartModel.fromMap(product)), 26 | ), 27 | totalPrice: map['total_price'], 28 | ); 29 | 30 | factory CartModel.fromEntity(Cart cart) => CartModel( 31 | products: cart.products 32 | .map((item) => ProductCartModel.fromEntity(item)) 33 | .toList(), 34 | totalPrice: cart.totalPrice, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/presentation/blocs/product/product_state.dart: -------------------------------------------------------------------------------- 1 | part of 'product_bloc.dart'; 2 | 3 | abstract class ProductState extends Equatable { 4 | const ProductState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class ProductInitial extends ProductState {} 11 | 12 | class ProductLoading extends ProductState {} 13 | 14 | class ProductFailure extends ProductState { 15 | final String message; 16 | 17 | const ProductFailure(this.message); 18 | 19 | @override 20 | List get props => [message]; 21 | } 22 | 23 | class ProductEmpty extends ProductState {} 24 | 25 | class AllProductsLoaded extends ProductState { 26 | final List products; 27 | 28 | const AllProductsLoaded(this.products); 29 | 30 | @override 31 | List get props => [products]; 32 | } 33 | 34 | class ProductActionSuccess extends ProductState { 35 | final String message; 36 | 37 | const ProductActionSuccess(this.message); 38 | 39 | @override 40 | List get props => [message]; 41 | } 42 | 43 | class ProductActionFailure extends ProductState { 44 | final String message; 45 | 46 | const ProductActionFailure(this.message); 47 | 48 | @override 49 | List get props => [message]; 50 | } 51 | -------------------------------------------------------------------------------- /lib/presentation/blocs/report/report_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/domain/usecases/report/get_count_transactions_today.dart'; 5 | import 'package:fic_mini_project/domain/usecases/report/get_turn_over_transactions_today.dart'; 6 | 7 | part 'report_event.dart'; 8 | part 'report_state.dart'; 9 | 10 | class ReportBloc extends Bloc { 11 | final GetCountTransactionsToday getCountTransactionsToday; 12 | final GetTurnOverTransactionsToday getTurnOverTransactionsToday; 13 | 14 | ReportBloc({ 15 | required this.getCountTransactionsToday, 16 | required this.getTurnOverTransactionsToday, 17 | }) : super(ReportInitial()) { 18 | on((event, emit) async { 19 | emit(ReportLoading()); 20 | 21 | final countTransactions = await getCountTransactionsToday.execute(); 22 | final turnOverTransactions = await getTurnOverTransactionsToday.execute(); 23 | 24 | emit( 25 | ReportLoaded( 26 | countTransactions: countTransactions, 27 | turnOverTransactions: turnOverTransactions, 28 | ), 29 | ); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/blocs/category/category_state.dart: -------------------------------------------------------------------------------- 1 | part of 'category_bloc.dart'; 2 | 3 | abstract class CategoryState extends Equatable { 4 | const CategoryState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class CategoryInitial extends CategoryState {} 11 | 12 | class CategoryLoading extends CategoryState {} 13 | 14 | class CategoryFailure extends CategoryState { 15 | final String message; 16 | 17 | const CategoryFailure(this.message); 18 | 19 | @override 20 | List get props => [message]; 21 | } 22 | 23 | class CategoryEmpty extends CategoryState {} 24 | 25 | class AllCategoriesLoaded extends CategoryState { 26 | final List categories; 27 | 28 | const AllCategoriesLoaded(this.categories); 29 | 30 | @override 31 | List get props => [categories]; 32 | } 33 | 34 | class CategoryActionSuccess extends CategoryState { 35 | final String message; 36 | 37 | const CategoryActionSuccess(this.message); 38 | 39 | @override 40 | List get props => [message]; 41 | } 42 | 43 | class CategoryActionFailure extends CategoryState { 44 | final String message; 45 | 46 | const CategoryActionFailure(this.message); 47 | 48 | @override 49 | List get props => [message]; 50 | } 51 | -------------------------------------------------------------------------------- /lib/data/models/point_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import 'package:fic_mini_project/domain/entity/point.dart'; 5 | 6 | class PointModel extends Equatable { 7 | final String userId; 8 | final int point; 9 | final bool isEntry; 10 | final DateTime createdAt; 11 | 12 | const PointModel({ 13 | required this.userId, 14 | required this.point, 15 | required this.isEntry, 16 | required this.createdAt, 17 | }); 18 | 19 | factory PointModel.fromSnapshot(DocumentSnapshot doc) => PointModel( 20 | userId: doc.get('user_id'), 21 | point: doc.get('point'), 22 | isEntry: doc.get('is_entry'), 23 | createdAt: DateTime.parse(doc.get('created_at')), 24 | ); 25 | 26 | Map toDocument() => { 27 | 'user_id': userId, 28 | 'point': point, 29 | 'is_entry': isEntry, 30 | 'created_at': createdAt.toIso8601String(), 31 | }; 32 | 33 | Point toEntity() => Point( 34 | userId: userId, 35 | point: point, 36 | isEntry: isEntry, 37 | createdAt: createdAt, 38 | ); 39 | 40 | @override 41 | List get props => [userId, point, isEntry, createdAt]; 42 | } 43 | -------------------------------------------------------------------------------- /lib/presentation/pages/member/scan_qr_code_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:mobile_scanner/mobile_scanner.dart'; 6 | 7 | import 'package:fic_mini_project/common/routes.dart'; 8 | import 'package:fic_mini_project/data/models/cart_model.dart'; 9 | 10 | class ScanQrCodePage extends StatelessWidget { 11 | const ScanQrCodePage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Scan QR Code'), 18 | ), 19 | body: MobileScanner( 20 | onDetect: (barcode) { 21 | final result = barcode.barcodes; 22 | if (result.first.rawValue != null) { 23 | try { 24 | final cartMap = 25 | jsonDecode(result.first.rawValue!) as Map; 26 | final cart = CartModel.fromMap(cartMap).toEntity(); 27 | Navigator.pushReplacementNamed( 28 | context, 29 | memberCheckoutRoute, 30 | arguments: cart, 31 | ); 32 | } catch (e) { 33 | debugPrint(e.toString()); 34 | } 35 | } 36 | }, 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fic_mini_project 2 | description: A new Flutter project. 3 | publish_to: 'none' 4 | version: 1.2.0+3 5 | 6 | environment: 7 | sdk: '>=2.18.5 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | cupertino_icons: ^1.0.6 13 | google_fonts: ^6.1.0 14 | font_awesome_flutter: ^10.6.0 15 | equatable: ^2.0.5 16 | dartz: ^0.10.1 17 | firebase_core: ^2.22.0 18 | firebase_auth: ^4.14.0 19 | google_sign_in: ^6.1.6 20 | flutter_bloc: ^8.1.3 21 | get_it: ^7.6.4 22 | shared_preferences: ^2.2.2 23 | cloud_firestore: ^4.13.1 24 | firebase_storage: ^11.5.1 25 | image_picker: ^1.0.4 26 | intl: ^0.18.1 27 | sqflite: ^2.3.0 28 | qr_flutter: ^4.1.0 29 | carousel_slider: ^4.2.1 30 | mobile_scanner: ^3.5.2 31 | avatar_glow: ^2.0.2 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | flutter_lints: ^3.0.1 37 | flutter_launcher_icons: ^0.13.1 38 | 39 | flutter: 40 | uses-material-design: true 41 | assets: 42 | - assets/logo/logo.png 43 | - assets/success_order.png 44 | - assets/banner_promo/ 45 | 46 | flutter_icons: 47 | android: true 48 | image_path: 'assets/logo/logo.png' 49 | adaptive_icon_background: 'assets/logo/background.png' 50 | adaptive_icon_foreground: 'assets/logo/foreground.png' 51 | -------------------------------------------------------------------------------- /lib/presentation/widgets/menu_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:fic_mini_project/common/styles.dart'; 4 | 5 | class MenuCard extends StatelessWidget { 6 | const MenuCard({ 7 | super.key, 8 | required this.onPressed, 9 | required this.icon, 10 | required this.labelText, 11 | }); 12 | 13 | final Function() onPressed; 14 | final Widget icon; 15 | final String labelText; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return InkWell( 20 | onTap: onPressed, 21 | borderRadius: BorderRadius.circular(16), 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.center, 24 | children: [ 25 | Container( 26 | height: 60, 27 | width: 60, 28 | decoration: BoxDecoration( 29 | color: navyColor, 30 | borderRadius: BorderRadius.circular(8), 31 | ), 32 | child: Center(child: icon), 33 | ), 34 | const SizedBox(height: 10), 35 | SizedBox( 36 | height: 30, 37 | child: Text( 38 | labelText, 39 | textAlign: TextAlign.center, 40 | maxLines: 2, 41 | ), 42 | ), 43 | ], 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/data/repositories/user_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import 'package:fic_mini_project/common/failure.dart'; 5 | import 'package:fic_mini_project/data/datasources/user_remote_data_source.dart'; 6 | import 'package:fic_mini_project/data/models/user_model.dart'; 7 | import 'package:fic_mini_project/domain/entity/user.dart'; 8 | import 'package:fic_mini_project/domain/repositories/user_repository.dart'; 9 | 10 | class UserRepositoryImpl extends UserRepository { 11 | final UserRemoteDataSource remoteDataSource; 12 | 13 | UserRepositoryImpl(this.remoteDataSource); 14 | 15 | @override 16 | Future> getCurrentUser() async { 17 | try { 18 | final result = await remoteDataSource.getCurrentUser(); 19 | return Right(result.toEntity()); 20 | } catch (e) { 21 | return Left(ServerFailure(e.toString())); 22 | } 23 | } 24 | 25 | @override 26 | Future> updateCurrentUser( 27 | User user, 28 | XFile? image, 29 | ) async { 30 | try { 31 | await remoteDataSource.updateCurrentUser( 32 | UserModel.fromEntity(user), 33 | image, 34 | ); 35 | return const Right('Berhasil update profile'); 36 | } catch (e) { 37 | return Left(ServerFailure(e.toString())); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/presentation/widgets/confirm_delete_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:fic_mini_project/common/styles.dart'; 4 | 5 | class ConfirmDeleteDialog extends StatelessWidget { 6 | const ConfirmDeleteDialog({ 7 | super.key, 8 | required this.yesOnPressed, 9 | required this.title, 10 | }); 11 | 12 | final Function()? yesOnPressed; 13 | final String title; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return AlertDialog( 18 | shape: RoundedRectangleBorder( 19 | borderRadius: BorderRadius.circular(16), 20 | ), 21 | title: Text( 22 | title, 23 | style: 24 | Theme.of(context).textTheme.titleLarge!.copyWith(color: navyColor), 25 | ), 26 | icon: Icon( 27 | Icons.delete_forever, 28 | color: Colors.red[400], 29 | ), 30 | content: const Text( 31 | 'Apakah Anda ingin menghapus ?', 32 | textAlign: TextAlign.center, 33 | ), 34 | actionsAlignment: MainAxisAlignment.center, 35 | actions: [ 36 | TextButton( 37 | onPressed: () => Navigator.pop(context), 38 | child: const Text('Tidak'), 39 | ), 40 | ElevatedButton( 41 | onPressed: yesOnPressed, 42 | child: const Text('Ya'), 43 | ), 44 | ], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/data/repositories/point_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/data/datasources/point_remote_data_source.dart'; 5 | import 'package:fic_mini_project/domain/entity/point.dart'; 6 | import 'package:fic_mini_project/domain/repositories/point_repository.dart'; 7 | 8 | class PointRepositoryImpl extends PointRepository { 9 | final PointRemoteDataSource remoteDataSource; 10 | 11 | PointRepositoryImpl(this.remoteDataSource); 12 | 13 | @override 14 | Future>> getAllPointsHistory() async { 15 | try { 16 | final result = await remoteDataSource.getAllPointsHistory(); 17 | return Right(result.map((point) => point.toEntity()).toList()); 18 | } catch (e) { 19 | return Left(ServerFailure(e.toString())); 20 | } 21 | } 22 | 23 | @override 24 | Future> savePoint(int point) async { 25 | try { 26 | final result = await remoteDataSource.savePoint(point); 27 | return Right(result); 28 | } catch (e) { 29 | return Left(ServerFailure(e.toString())); 30 | } 31 | } 32 | 33 | @override 34 | Future> usePoint(int point) async { 35 | try { 36 | final result = await remoteDataSource.usePoint(point); 37 | return Right(result); 38 | } catch (e) { 39 | return Left(ServerFailure(e.toString())); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/presentation/blocs/profile/profile_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:image_picker/image_picker.dart'; 4 | 5 | import 'package:fic_mini_project/domain/entity/user.dart'; 6 | import 'package:fic_mini_project/domain/usecases/user/get_current_user.dart'; 7 | import 'package:fic_mini_project/domain/usecases/user/update_current_user.dart'; 8 | 9 | part 'profile_event.dart'; 10 | part 'profile_state.dart'; 11 | 12 | class ProfileBloc extends Bloc { 13 | final GetCurrentUser getCurrentUser; 14 | final UpdateCurrentUser updateCurrentUser; 15 | 16 | ProfileBloc({ 17 | required this.getCurrentUser, 18 | required this.updateCurrentUser, 19 | }) : super(ProfileInitial()) { 20 | on((event, emit) async { 21 | emit(ProfileLoading()); 22 | 23 | final result = await getCurrentUser.execute(); 24 | 25 | result.fold( 26 | (failure) => emit(ProfileFailure(failure.message)), 27 | (data) => emit(ProfileLoaded(data)), 28 | ); 29 | }); 30 | 31 | on((event, emit) async { 32 | emit(ProfileLoading()); 33 | 34 | final result = await updateCurrentUser.execute(event.user, event.image); 35 | 36 | result.fold( 37 | (failure) => emit(ProfileUpdateFailure(failure.message)), 38 | (successMessage) => emit(ProfileUpdateSuccess(successMessage)), 39 | ); 40 | 41 | add(OnFetchProfile()); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/data/datasources/report_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | abstract class ReportRemoteDataSource { 5 | Future getCountTransactionsToday(); 6 | Future getTurnOverTransactionsToday(); 7 | } 8 | 9 | class ReportRemoteDataSourceImpl extends ReportRemoteDataSource { 10 | FirebaseFirestore firebaseFirestore; 11 | 12 | ReportRemoteDataSourceImpl(this.firebaseFirestore); 13 | 14 | @override 15 | Future getCountTransactionsToday() async { 16 | final transactionCollection = 17 | await firebaseFirestore.collection('transactions').get(); 18 | 19 | return transactionCollection.docs 20 | .where( 21 | (element) => element 22 | .data()['created_at'] 23 | .toString() 24 | .contains(DateFormat('yyyy-MM-dd').format(DateTime.now())), 25 | ) 26 | .length; 27 | } 28 | 29 | @override 30 | Future getTurnOverTransactionsToday() async { 31 | int sum = 0; 32 | 33 | final transactionCollection = 34 | await firebaseFirestore.collection('transactions').get(); 35 | 36 | final docs = transactionCollection.docs.where( 37 | (element) => element 38 | .data()['created_at'] 39 | .toString() 40 | .contains(DateFormat('yyyy-MM-dd').format(DateTime.now())), 41 | ); 42 | 43 | for (var element in docs) { 44 | final int totalPrice = element.data()['end_total_price']; 45 | sum += totalPrice; 46 | } 47 | 48 | return sum; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/presentation/widgets/action_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ActionDialog extends StatelessWidget { 4 | const ActionDialog({ 5 | super.key, 6 | required this.titleUpdateAction, 7 | required this.titleDeleteAction, 8 | required this.updateActionOnTap, 9 | required this.deleteActionOnTap, 10 | }); 11 | 12 | final String titleUpdateAction; 13 | final String titleDeleteAction; 14 | final Function()? updateActionOnTap; 15 | final Function()? deleteActionOnTap; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return AlertDialog( 20 | insetPadding: const EdgeInsets.symmetric( 21 | horizontal: 42, 22 | vertical: 16, 23 | ), 24 | shape: RoundedRectangleBorder( 25 | borderRadius: BorderRadius.circular(8), 26 | ), 27 | title: const Text('Aksi'), 28 | content: SizedBox( 29 | width: MediaQuery.of(context).size.width, 30 | child: SingleChildScrollView( 31 | child: Column( 32 | children: [ 33 | ListTile( 34 | onTap: updateActionOnTap, 35 | title: Text(titleUpdateAction), 36 | leading: const Icon(Icons.edit), 37 | trailing: const Icon(Icons.arrow_forward_ios), 38 | ), 39 | ListTile( 40 | onTap: deleteActionOnTap, 41 | title: Text(titleDeleteAction), 42 | leading: const Icon(Icons.delete_forever), 43 | trailing: const Icon(Icons.arrow_forward_ios), 44 | ), 45 | ], 46 | ), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/data/repositories/transaction_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/failure.dart'; 4 | import 'package:fic_mini_project/data/datasources/transaction_remote_data_source.dart'; 5 | import 'package:fic_mini_project/data/models/transaction_model.dart'; 6 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 7 | import 'package:fic_mini_project/domain/repositories/transaction_repository.dart'; 8 | 9 | class TransactionRepositoryImpl extends TransactionRepository { 10 | final TransactionRemoteDataSource remoteDataSource; 11 | 12 | TransactionRepositoryImpl(this.remoteDataSource); 13 | 14 | @override 15 | Future>> getAllTransactions() async { 16 | try { 17 | final result = await remoteDataSource.getAllTransactions(); 18 | return Right(result.map((item) => item.toEntity()).toList()); 19 | } catch (e) { 20 | return Left(ServerFailure(e.toString())); 21 | } 22 | } 23 | 24 | @override 25 | Future>> 26 | getAllTransactionsByUserId() async { 27 | try { 28 | final result = await remoteDataSource.getAllTransactionsByUserId(); 29 | return Right(result.map((item) => item.toEntity()).toList()); 30 | } catch (e) { 31 | return Left(ServerFailure(e.toString())); 32 | } 33 | } 34 | 35 | @override 36 | Future> saveTransaction( 37 | TransactionEntity transaction, 38 | ) async { 39 | try { 40 | final result = await remoteDataSource.saveTransaction( 41 | TransactionModel.fromEntity(transaction), 42 | ); 43 | return Right(result); 44 | } catch (e) { 45 | return Left(ServerFailure(e.toString())); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 17 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 18 | - platform: android 19 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 20 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 21 | - platform: ios 22 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 23 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 24 | - platform: linux 25 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 26 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 27 | - platform: macos 28 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 29 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 30 | - platform: web 31 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 32 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 33 | - platform: windows 34 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 35 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /lib/data/models/product_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | 5 | import 'package:fic_mini_project/data/models/category_model.dart'; 6 | import 'package:fic_mini_project/domain/entity/product.dart'; 7 | 8 | class ProductModel extends Equatable { 9 | final int? id; 10 | final String name; 11 | final int price; 12 | final CategoryModel category; 13 | final Uint8List image; 14 | 15 | const ProductModel({ 16 | required this.id, 17 | required this.name, 18 | required this.price, 19 | required this.category, 20 | required this.image, 21 | }); 22 | 23 | factory ProductModel.fromMap(Map map) => ProductModel( 24 | id: map['id'], 25 | name: map['name'], 26 | price: map['price'], 27 | category: CategoryModel.fromMap({ 28 | 'id': map['category_id'], 29 | 'name': map['category_name'], 30 | }), 31 | image: map['image'], 32 | ); 33 | 34 | factory ProductModel.fromEntity(Product product) => ProductModel( 35 | id: product.id, 36 | name: product.name, 37 | price: product.price, 38 | category: CategoryModel.fromEntity(product.category!), 39 | image: product.image!, 40 | ); 41 | 42 | Map toMap() => { 43 | 'id': id, 44 | 'name': name, 45 | 'price': price, 46 | 'category_id': category.id, 47 | 'image': image, 48 | }; 49 | 50 | Product toEntity() => Product( 51 | id: id, 52 | name: name, 53 | price: price, 54 | category: category.toEntity(), 55 | image: image, 56 | ); 57 | 58 | @override 59 | List get props => [ 60 | id, 61 | name, 62 | price, 63 | category, 64 | image, 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "266552192086", 4 | "project_id": "palem-cafe", 5 | "storage_bucket": "palem-cafe.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:266552192086:android:d2bbd8950232d4d6ae9ff7", 11 | "android_client_info": { 12 | "package_name": "com.triagung.fic_mini_project" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "266552192086-3pf1hqd4cbb3sof8l6g0h5t1sl682j9a.apps.googleusercontent.com", 18 | "client_type": 1, 19 | "android_info": { 20 | "package_name": "com.triagung.fic_mini_project", 21 | "certificate_hash": "930bb0ec000cccf51802f307c7400fe595d9477a" 22 | } 23 | }, 24 | { 25 | "client_id": "266552192086-mhdamiirq56ha6ni1uftiuaafdk0540g.apps.googleusercontent.com", 26 | "client_type": 1, 27 | "android_info": { 28 | "package_name": "com.triagung.fic_mini_project", 29 | "certificate_hash": "0a777e4bdbfd42f4ec3540c6a19726a4829f7b77" 30 | } 31 | }, 32 | { 33 | "client_id": "266552192086-euviurh5av97343g1rk01a34s4prqfj2.apps.googleusercontent.com", 34 | "client_type": 3 35 | } 36 | ], 37 | "api_key": [ 38 | { 39 | "current_key": "AIzaSyCrMkUTLxCVae53thx82Cl5wwBFIPVjT68" 40 | } 41 | ], 42 | "services": { 43 | "appinvite_service": { 44 | "other_platform_oauth_client": [ 45 | { 46 | "client_id": "266552192086-euviurh5av97343g1rk01a34s4prqfj2.apps.googleusercontent.com", 47 | "client_type": 3 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | ], 54 | "configuration_version": "1" 55 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/data/datasources/product_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/common/exception.dart'; 2 | import 'package:fic_mini_project/data/db/database_helper.dart'; 3 | import 'package:fic_mini_project/data/models/product_model.dart'; 4 | 5 | abstract class ProductLocalDataSource { 6 | Future> getAllProducts(); 7 | Future insertProduct(ProductModel product); 8 | Future updateProduct(ProductModel product); 9 | Future removeProduct(ProductModel product); 10 | } 11 | 12 | class ProductLocalDataSourceImpl extends ProductLocalDataSource { 13 | final DatabaseHelper databaseHelper; 14 | 15 | ProductLocalDataSourceImpl(this.databaseHelper); 16 | 17 | @override 18 | Future> getAllProducts() async { 19 | try { 20 | final result = await databaseHelper.getAllProducts(); 21 | return result.map((product) => ProductModel.fromMap(product)).toList(); 22 | } catch (e) { 23 | throw DatabaseException(e.toString()); 24 | } 25 | } 26 | 27 | @override 28 | Future insertProduct(ProductModel product) async { 29 | try { 30 | await databaseHelper.insertProduct(product); 31 | return 'Berhasil menambahkan produk'; 32 | } catch (e) { 33 | throw DatabaseException(e.toString()); 34 | } 35 | } 36 | 37 | @override 38 | Future removeProduct(ProductModel product) async { 39 | try { 40 | await databaseHelper.removeProduct(product); 41 | return 'Berhasil menghapus produk'; 42 | } catch (e) { 43 | throw DatabaseException(e.toString()); 44 | } 45 | } 46 | 47 | @override 48 | Future updateProduct(ProductModel product) async { 49 | try { 50 | await databaseHelper.updateProduct(product); 51 | return 'Berhasil update produk'; 52 | } catch (e) { 53 | throw DatabaseException(e.toString()); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/data/datasources/category_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/common/exception.dart'; 2 | import 'package:fic_mini_project/data/db/database_helper.dart'; 3 | import 'package:fic_mini_project/data/models/category_model.dart'; 4 | 5 | abstract class CategoryLocalDataSource { 6 | Future> getAllCategories(); 7 | Future insertCategory(CategoryModel category); 8 | Future updateCategory(CategoryModel category); 9 | Future removeCategory(CategoryModel category); 10 | } 11 | 12 | class CategoryLocalDataSourceImpl extends CategoryLocalDataSource { 13 | final DatabaseHelper databaseHelper; 14 | 15 | CategoryLocalDataSourceImpl(this.databaseHelper); 16 | 17 | @override 18 | Future> getAllCategories() async { 19 | try { 20 | final result = await databaseHelper.getAllCategories(); 21 | return result.map((category) => CategoryModel.fromMap(category)).toList(); 22 | } catch (e) { 23 | throw DatabaseException(e.toString()); 24 | } 25 | } 26 | 27 | @override 28 | Future insertCategory(CategoryModel category) async { 29 | try { 30 | await databaseHelper.insertCategory(category); 31 | return 'Berhasil menambahkan kategori'; 32 | } catch (e) { 33 | throw DatabaseException(e.toString()); 34 | } 35 | } 36 | 37 | @override 38 | Future removeCategory(CategoryModel category) async { 39 | try { 40 | await databaseHelper.removeCategory(category); 41 | return 'Berhasil menghapus kategori'; 42 | } catch (e) { 43 | throw DatabaseException(e.toString()); 44 | } 45 | } 46 | 47 | @override 48 | Future updateCategory(CategoryModel category) async { 49 | try { 50 | await databaseHelper.updateCategory(category); 51 | return 'Berhasil update kategori'; 52 | } catch (e) { 53 | throw DatabaseException(e.toString()); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/data/repositories/cart_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:fic_mini_project/data/models/cart_model.dart'; 2 | import 'package:fic_mini_project/data/models/product_cart_model.dart'; 3 | import 'package:fic_mini_project/domain/entity/cart.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/domain/repositories/cart_repository.dart'; 6 | 7 | class CartRepositoryImpl extends CartRepository { 8 | final CartModel _carts = CartModel(products: [], totalPrice: 0); 9 | 10 | int getTotalPrice() { 11 | int total = 0; 12 | for (var item in _carts.products) { 13 | total += (item.quantity * item.price); 14 | } 15 | return total; 16 | } 17 | 18 | @override 19 | Future addProductToCart(Product product) async { 20 | final productFromEntity = ProductCartModel.fromEntity(product) 21 | ..quantity = 1; 22 | _carts.products.add(productFromEntity); 23 | _carts.totalPrice = getTotalPrice(); 24 | 25 | return _carts.toEntity(); 26 | } 27 | 28 | @override 29 | Future addProductQuantity(Product product) async { 30 | int index = _carts.products.indexWhere( 31 | (item) => item.id == ProductCartModel.fromEntity(product).id, 32 | ); 33 | _carts.products[index].quantity++; 34 | _carts.totalPrice = getTotalPrice(); 35 | 36 | return _carts.toEntity(); 37 | } 38 | 39 | @override 40 | Future reduceProductQuantity(Product product) async { 41 | int index = _carts.products.indexWhere( 42 | (item) => item.id == ProductCartModel.fromEntity(product).id, 43 | ); 44 | _carts.products[index].quantity--; 45 | 46 | if (_carts.products[index].quantity == 0) _carts.products.removeAt(index); 47 | 48 | _carts.totalPrice = getTotalPrice(); 49 | 50 | return _carts.toEntity(); 51 | } 52 | 53 | @override 54 | Future clearCart() async { 55 | _carts.products.clear(); 56 | _carts.totalPrice = 0; 57 | } 58 | 59 | @override 60 | Future> getAllCartsMap() async { 61 | return _carts.toMap(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/data/datasources/user_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:firebase_storage/firebase_storage.dart'; 6 | import 'package:image_picker/image_picker.dart'; 7 | 8 | import 'package:fic_mini_project/data/models/user_model.dart'; 9 | 10 | abstract class UserRemoteDataSource { 11 | Future getCurrentUser(); 12 | Future updateCurrentUser(UserModel user, XFile? image); 13 | } 14 | 15 | class UserRemoteDataSourceImpl extends UserRemoteDataSource { 16 | final FirebaseAuth firebaseAuth; 17 | final FirebaseFirestore firebaseFirestore; 18 | final FirebaseStorage firebaseStorage; 19 | 20 | UserRemoteDataSourceImpl({ 21 | required this.firebaseAuth, 22 | required this.firebaseFirestore, 23 | required this.firebaseStorage, 24 | }); 25 | 26 | @override 27 | Future getCurrentUser() async { 28 | final currentUser = firebaseAuth.currentUser!; 29 | final userCollection = 30 | firebaseFirestore.collection('users').doc(currentUser.uid); 31 | final documentSnapshot = await userCollection.get(); 32 | return UserModel.fromSnapshot(documentSnapshot); 33 | } 34 | 35 | @override 36 | Future updateCurrentUser(UserModel user, XFile? image) async { 37 | if (image != null) { 38 | final fileExtension = image.name.split('.').last; 39 | final fileName = 'profile.$fileExtension'; 40 | final file = File(image.path); 41 | 42 | final storageRef = firebaseStorage.ref(user.id).child(fileName); 43 | await storageRef.putFile(file); 44 | final imageUrl = await storageRef.getDownloadURL(); 45 | 46 | user = UserModel( 47 | id: user.id, 48 | name: user.name, 49 | email: user.email, 50 | phoneNumber: user.phoneNumber, 51 | photoUrl: imageUrl, 52 | point: user.point, 53 | ); 54 | } 55 | 56 | firebaseFirestore 57 | .collection('users') 58 | .doc(user.id) 59 | .update(user.toDocument()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/presentation/blocs/auth/auth_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/domain/usecases/auth/get_login_status.dart'; 5 | import 'package:fic_mini_project/domain/usecases/auth/get_role.dart'; 6 | import 'package:fic_mini_project/domain/usecases/auth/login_with_google.dart'; 7 | import 'package:fic_mini_project/domain/usecases/auth/logout.dart'; 8 | import 'package:fic_mini_project/domain/usecases/auth/set_role.dart'; 9 | 10 | part 'auth_event.dart'; 11 | part 'auth_state.dart'; 12 | 13 | class AuthBloc extends Bloc { 14 | static const String roleVendor = 'ROLE_VENDOR'; 15 | static const String roleMember = 'ROLE_MEMBER'; 16 | 17 | final Login login; 18 | final GetLoginStatus getLoginStatus; 19 | final Logout logout; 20 | final SetRole setRole; 21 | final GetRole getRole; 22 | 23 | AuthBloc({ 24 | required this.login, 25 | required this.getLoginStatus, 26 | required this.logout, 27 | required this.setRole, 28 | required this.getRole, 29 | }) : super(AuthInitial()) { 30 | on((event, emit) async { 31 | emit(AuthLoading()); 32 | 33 | final result = await login.execute(); 34 | result.fold( 35 | (failure) => emit(AuthFailure(failure.message)), 36 | (user) { 37 | if (user.role == 'vendor') { 38 | setRole.execute(roleVendor); 39 | emit(LoginAsVendorAuthenticated()); 40 | } else { 41 | setRole.execute(roleMember); 42 | emit(LoginAsMemberAuthenticated()); 43 | } 44 | }, 45 | ); 46 | }); 47 | 48 | on((event, emit) async { 49 | final status = getLoginStatus.execute(); 50 | final role = await getRole.execute(); 51 | 52 | await Future.delayed(const Duration(seconds: 1)); 53 | 54 | emit(LoginStatusLoaded(status: status, role: role ?? '')); 55 | }); 56 | 57 | on((event, emit) async { 58 | await logout.execute(); 59 | emit(LogoutSuccess()); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/data/repositories/auth_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:google_sign_in/google_sign_in.dart'; 6 | 7 | import 'package:fic_mini_project/common/failure.dart'; 8 | import 'package:fic_mini_project/data/datasources/auth_local_data_source.dart'; 9 | import 'package:fic_mini_project/data/datasources/auth_remote_data_source.dart'; 10 | import 'package:fic_mini_project/domain/entity/user.dart'; 11 | import 'package:fic_mini_project/domain/repositories/auth_repository.dart'; 12 | 13 | class AuthRepositoryImpl implements AuthRepository { 14 | final AuthRemoteDataSource remoteDataSource; 15 | final AuthLocalDataSource localDataSource; 16 | 17 | AuthRepositoryImpl({ 18 | required this.remoteDataSource, 19 | required this.localDataSource, 20 | }); 21 | 22 | @override 23 | Future> loginWithGoogle() async { 24 | try { 25 | final result = await remoteDataSource.loginWithGoogle(); 26 | return Right(result.toEntity()); 27 | } on PlatformException catch (e) { 28 | if (e.code == GoogleSignIn.kNetworkError) { 29 | return const Left( 30 | GoogleSignInFailure('Failed to connect to the network'), 31 | ); 32 | } else if (e.code == GoogleSignIn.kSignInCanceledError) { 33 | return const Left(GoogleSignInFailure('')); 34 | } else { 35 | return const Left(GoogleSignInFailure('Something went wrong')); 36 | } 37 | } 38 | } 39 | 40 | @override 41 | bool getLoginStatus() { 42 | return remoteDataSource.isLogin(); 43 | } 44 | 45 | @override 46 | Future logout() async { 47 | await localDataSource.removeRole(); 48 | await remoteDataSource.logout(); 49 | } 50 | 51 | @override 52 | Future getRole() async { 53 | try { 54 | return await localDataSource.getRole(); 55 | } catch (e) { 56 | debugPrint(e.toString()); 57 | return null; 58 | } 59 | } 60 | 61 | @override 62 | Future setRole(String role) async { 63 | try { 64 | await localDataSource.setRole(role); 65 | } catch (e) { 66 | debugPrint(e.toString()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/presentation/pages/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | import 'package:fic_mini_project/common/routes.dart'; 7 | import 'package:fic_mini_project/presentation/blocs/auth/auth_bloc.dart'; 8 | 9 | class SplashPage extends StatefulWidget { 10 | const SplashPage({super.key}); 11 | 12 | @override 13 | State createState() => _SplashPageState(); 14 | } 15 | 16 | class _SplashPageState extends State { 17 | @override 18 | void initState() { 19 | super.initState(); 20 | context.read().add(LoadLoginStatus()); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return AnnotatedRegion( 26 | value: SystemUiOverlayStyle.dark, 27 | child: Scaffold( 28 | body: BlocListener( 29 | listener: (context, state) { 30 | if (state is LoginStatusLoaded) { 31 | Navigator.pushNamedAndRemoveUntil( 32 | context, 33 | state.status == true 34 | ? state.role == AuthBloc.roleVendor 35 | ? vendorHomeRoute 36 | : memberHomeRoute 37 | : loginRoute, 38 | (_) => false, 39 | ); 40 | } 41 | }, 42 | child: SafeArea( 43 | child: Center( 44 | child: Column( 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | children: [ 47 | Container( 48 | width: 200, 49 | height: 200, 50 | decoration: BoxDecoration( 51 | image: const DecorationImage( 52 | image: AssetImage('assets/logo/logo.png'), 53 | ), 54 | borderRadius: BorderRadius.circular(16), 55 | ), 56 | ), 57 | const SizedBox(height: 48), 58 | const CircularProgressIndicator(), 59 | ], 60 | ), 61 | ), 62 | ), 63 | ), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/presentation/blocs/transaction/transaction_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 5 | import 'package:fic_mini_project/domain/usecases/transaction/get_all_transactions.dart'; 6 | import 'package:fic_mini_project/domain/usecases/transaction/get_all_transactions_by_user_id.dart'; 7 | import 'package:fic_mini_project/domain/usecases/transaction/save_transaction.dart'; 8 | 9 | part 'transaction_event.dart'; 10 | part 'transaction_state.dart'; 11 | 12 | class TransactionBloc extends Bloc { 13 | final GetAllTransactions getAllTransactions; 14 | final GetAllTransactionsByUserId getAllTransactionsByUserId; 15 | final SaveTransaction saveTransaction; 16 | 17 | TransactionBloc({ 18 | required this.getAllTransactions, 19 | required this.getAllTransactionsByUserId, 20 | required this.saveTransaction, 21 | }) : super(TransactionInitial()) { 22 | on((event, emit) async { 23 | emit(TransactionLoading()); 24 | 25 | final result = await getAllTransactions.execute(); 26 | 27 | result.fold( 28 | (failure) => emit(TransactionFailure(failure.message)), 29 | (data) { 30 | if (data.isEmpty) { 31 | emit(TransactionEmpty()); 32 | } else { 33 | emit(AllTransactionsLoaded(data)); 34 | } 35 | }, 36 | ); 37 | }); 38 | 39 | on((event, emit) async { 40 | emit(TransactionLoading()); 41 | 42 | final result = await getAllTransactionsByUserId.execute(); 43 | 44 | result.fold( 45 | (failure) => emit(TransactionFailure(failure.message)), 46 | (data) { 47 | if (data.isEmpty) { 48 | emit(TransactionEmpty()); 49 | } else { 50 | emit(AllTransactionsLoaded(data)); 51 | } 52 | }, 53 | ); 54 | }); 55 | 56 | on((event, emit) async { 57 | emit(TransactionLoading()); 58 | 59 | final result = await saveTransaction.execute(event.transaction); 60 | 61 | result.fold( 62 | (failure) => emit(TransactionFailure(failure.message)), 63 | (_) => emit(TransactionSuccess()), 64 | ); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/data/models/user_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import 'package:fic_mini_project/domain/entity/user.dart'; 5 | 6 | class UserModel extends Equatable { 7 | final String id; 8 | final String? name; 9 | final String? email; 10 | final String? phoneNumber; 11 | final String? photoUrl; 12 | final String? role; 13 | final int? point; 14 | 15 | const UserModel({ 16 | required this.id, 17 | this.name, 18 | this.email, 19 | this.phoneNumber, 20 | this.photoUrl, 21 | this.role, 22 | this.point, 23 | }); 24 | 25 | @override 26 | List get props => [ 27 | id, 28 | name, 29 | email, 30 | phoneNumber, 31 | photoUrl, 32 | role, 33 | point, 34 | ]; 35 | 36 | User toEntity() => User( 37 | id: id, 38 | name: name, 39 | email: email, 40 | phoneNumber: phoneNumber, 41 | photoUrl: photoUrl, 42 | role: role, 43 | point: point, 44 | ); 45 | 46 | factory UserModel.fromEntity(User user) => UserModel( 47 | id: user.id, 48 | name: user.name, 49 | email: user.email, 50 | phoneNumber: user.phoneNumber, 51 | photoUrl: user.photoUrl, 52 | role: user.role, 53 | point: user.point, 54 | ); 55 | 56 | factory UserModel.fromSnapshot(DocumentSnapshot snapshot) => UserModel( 57 | id: snapshot.get('id'), 58 | name: snapshot.get('name'), 59 | email: snapshot.get('email'), 60 | phoneNumber: snapshot.get('phoneNumber'), 61 | photoUrl: snapshot.get('photoUrl'), 62 | role: snapshot.get('role'), 63 | point: snapshot.get('point'), 64 | ); 65 | 66 | factory UserModel.fromMap(Map map) => UserModel( 67 | id: map['id'], 68 | name: map['name'], 69 | email: map['email'], 70 | phoneNumber: map['phoneNumber'], 71 | photoUrl: map['photoUrl'], 72 | role: map['role'], 73 | point: map['point'], 74 | ); 75 | 76 | Map toDocument() => { 77 | 'id': id, 78 | 'name': name, 79 | 'email': email, 80 | 'phoneNumber': phoneNumber, 81 | 'photoUrl': photoUrl, 82 | 'role': role, 83 | 'point': point, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /lib/common/styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:google_fonts/google_fonts.dart'; 4 | 5 | const Color blueColor = Color(0xFF1A72DD); 6 | const Color navyColor = Color(0xFF2A3256); 7 | const Color whiteColor = Color(0xFFFFFFFF); 8 | const Color greyColor = Color(0xFFF7F8FA); 9 | 10 | final textTheme = TextTheme( 11 | displayLarge: GoogleFonts.rubik( 12 | fontSize: 98, 13 | fontWeight: FontWeight.w300, 14 | letterSpacing: -1.5, 15 | color: navyColor, 16 | ), 17 | displayMedium: GoogleFonts.rubik( 18 | fontSize: 61, 19 | fontWeight: FontWeight.w300, 20 | letterSpacing: -0.5, 21 | color: navyColor, 22 | ), 23 | displaySmall: GoogleFonts.rubik( 24 | fontSize: 49, 25 | fontWeight: FontWeight.w400, 26 | color: navyColor, 27 | ), 28 | headlineMedium: GoogleFonts.rubik( 29 | fontSize: 35, 30 | fontWeight: FontWeight.w400, 31 | letterSpacing: 0.25, 32 | color: navyColor, 33 | ), 34 | headlineSmall: GoogleFonts.rubik( 35 | fontSize: 24, 36 | fontWeight: FontWeight.w400, 37 | color: navyColor, 38 | ), 39 | titleLarge: GoogleFonts.rubik( 40 | fontSize: 20, 41 | fontWeight: FontWeight.w500, 42 | letterSpacing: 0.15, 43 | color: navyColor, 44 | ), 45 | titleMedium: GoogleFonts.rubik( 46 | fontSize: 16, 47 | fontWeight: FontWeight.w400, 48 | letterSpacing: 0.15, 49 | color: navyColor, 50 | ), 51 | titleSmall: GoogleFonts.rubik( 52 | fontSize: 14, 53 | fontWeight: FontWeight.w500, 54 | letterSpacing: 0.1, 55 | color: navyColor, 56 | ), 57 | bodyLarge: GoogleFonts.rubik( 58 | fontSize: 16, 59 | fontWeight: FontWeight.w400, 60 | letterSpacing: 0.5, 61 | color: navyColor, 62 | ), 63 | bodyMedium: GoogleFonts.rubik( 64 | fontSize: 14, 65 | fontWeight: FontWeight.w400, 66 | letterSpacing: 0.25, 67 | color: navyColor, 68 | ), 69 | labelLarge: GoogleFonts.rubik( 70 | fontSize: 14, 71 | fontWeight: FontWeight.w500, 72 | letterSpacing: 1.25, 73 | color: whiteColor, 74 | ), 75 | bodySmall: GoogleFonts.rubik( 76 | fontSize: 12, 77 | fontWeight: FontWeight.w400, 78 | letterSpacing: 0.4, 79 | color: navyColor, 80 | ), 81 | labelSmall: GoogleFonts.rubik( 82 | fontSize: 10, 83 | fontWeight: FontWeight.w400, 84 | letterSpacing: 1.5, 85 | color: navyColor, 86 | ), 87 | ); 88 | -------------------------------------------------------------------------------- /lib/data/repositories/category_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/exception.dart'; 4 | import 'package:fic_mini_project/common/failure.dart'; 5 | import 'package:fic_mini_project/data/datasources/category_local_data_source.dart'; 6 | import 'package:fic_mini_project/data/models/category_model.dart'; 7 | import 'package:fic_mini_project/domain/entity/category.dart'; 8 | import 'package:fic_mini_project/domain/repositories/category_repository.dart'; 9 | 10 | class CategoryRepositoryImpl extends CategoryRepository { 11 | final CategoryLocalDataSource localDataSource; 12 | 13 | CategoryRepositoryImpl(this.localDataSource); 14 | 15 | @override 16 | Future>> getAllCategories() async { 17 | try { 18 | final result = await localDataSource.getAllCategories(); 19 | return Right(result.map((category) => category.toEntity()).toList()); 20 | } on DatabaseException catch (_) { 21 | return const Left(DatabaseFailure('Gagal mendapatkan data kategori')); 22 | } catch (e) { 23 | rethrow; 24 | } 25 | } 26 | 27 | @override 28 | Future> insertCategory(Category category) async { 29 | try { 30 | final result = await localDataSource 31 | .insertCategory(CategoryModel.fromEntity(category)); 32 | return Right(result); 33 | } on DatabaseException catch (_) { 34 | return const Left(DatabaseFailure('Gagal tambah kategori')); 35 | } catch (e) { 36 | rethrow; 37 | } 38 | } 39 | 40 | @override 41 | Future> removeCategory(Category category) async { 42 | try { 43 | final result = await localDataSource 44 | .removeCategory(CategoryModel.fromEntity(category)); 45 | return Right(result); 46 | } on DatabaseException catch (_) { 47 | return const Left(DatabaseFailure('Gagal hapus kategori')); 48 | } catch (e) { 49 | rethrow; 50 | } 51 | } 52 | 53 | @override 54 | Future> updateCategory(Category category) async { 55 | try { 56 | final result = await localDataSource 57 | .updateCategory(CategoryModel.fromEntity(category)); 58 | return Right(result); 59 | } on DatabaseException catch (_) { 60 | return const Left(DatabaseFailure('Gagal update kategori')); 61 | } catch (e) { 62 | rethrow; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/data/repositories/product_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import 'package:fic_mini_project/common/exception.dart'; 4 | import 'package:fic_mini_project/common/failure.dart'; 5 | import 'package:fic_mini_project/data/datasources/product_local_data_source.dart'; 6 | import 'package:fic_mini_project/data/models/product_model.dart'; 7 | import 'package:fic_mini_project/domain/entity/product.dart'; 8 | import 'package:fic_mini_project/domain/repositories/product_repository.dart'; 9 | 10 | class ProductRepositoryImpl extends ProductRepository { 11 | final ProductLocalDataSource localDataSource; 12 | 13 | ProductRepositoryImpl(this.localDataSource); 14 | 15 | @override 16 | Future>> getAllProducts() async { 17 | try { 18 | final result = await localDataSource.getAllProducts(); 19 | return Right(result.map((product) => product.toEntity()).toList()); 20 | } on DatabaseException catch (_) { 21 | return const Left(DatabaseFailure('Gagal mendapatkan data produk')); 22 | } catch (e) { 23 | rethrow; 24 | } 25 | } 26 | 27 | @override 28 | Future> insertProduct(Product product) async { 29 | try { 30 | final result = await localDataSource.insertProduct( 31 | ProductModel.fromEntity(product), 32 | ); 33 | return Right(result); 34 | } on DatabaseException catch (_) { 35 | return const Left(DatabaseFailure('Gagal menambahkan produk')); 36 | } catch (e) { 37 | rethrow; 38 | } 39 | } 40 | 41 | @override 42 | Future> removeProduct(Product product) async { 43 | try { 44 | final result = await localDataSource.removeProduct( 45 | ProductModel.fromEntity(product), 46 | ); 47 | return Right(result); 48 | } on DatabaseException catch (_) { 49 | return const Left(DatabaseFailure('Gagal menghapus produk')); 50 | } catch (e) { 51 | rethrow; 52 | } 53 | } 54 | 55 | @override 56 | Future> updateProduct(Product product) async { 57 | try { 58 | final result = await localDataSource.updateProduct( 59 | ProductModel.fromEntity(product), 60 | ); 61 | return Right(result); 62 | } on DatabaseException catch (_) { 63 | return const Left(DatabaseFailure('Gagal update produk')); 64 | } catch (e) { 65 | rethrow; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Palem Kafe POS App 2 | Palem Kafe POS App adalah aplikasi berbasis android mobile yang mempermudah proses transaksi jual beli, mengelola data transaksi, dan mempermudah pemrosesan pembayaran. Fitur utamanya termasuk sistem kasir, pengelolaan inventori, pemrosesan pembayaran, dan menampilkan ringkasan laporan bisnis. 3 | 4 | Project ini merupakan sebuah challenge mini-project program FIC (Flutter Intensive Club) yang diadakan oleh FUGI (Flutter User Group Indonesia). 5 | 6 | ![Thumbnail](https://github.com/triagung128/fic-mini-project/blob/master/assets/screenshots/screenshot_1.png) 7 | 8 | ## :iphone: Download App 9 | Get it on Google Play 10 | 11 | ## Demo App 12 | https://youtu.be/5TVLj3m-B5g 13 | 14 | ## Features App 15 | Aplikasi ini terdiri dari 2 role user, yaitu vendor dan member. 16 | ### Vendor 17 | - Login by Google 18 | - Dashboard Menu 19 | - Category Management 20 | - Product Management 21 | - Point Of Sales 22 | - History Transactions 23 | - My Profile 24 | ### Member 25 | - Login by Google 26 | - Home Page 27 | - Member Points 28 | - Scan QR Code 29 | - Checkout 30 | - History Transactions 31 | - My Profile 32 | 33 | ## State Management 34 | - Flutter Bloc 35 | 36 | ## Dependencies 37 | - avatar_glow 38 | - carousel_slider 39 | - cloud_firestore 40 | - dartz 41 | - equatable 42 | - firebase_auth 43 | - firebase_core 44 | - firebase_storage 45 | - flutter_bloc 46 | - font_awesome_flutter 47 | - get_it 48 | - google_fonts 49 | - google_sign_in 50 | - image_picker 51 | - intl 52 | - mobile_scanner 53 | - qr_flutter 54 | - shared_preferences 55 | - sqflite 56 | 57 | ## Screenshots 58 | ![Screenshots 1](https://github.com/triagung128/fic-mini-project/blob/master/assets/screenshots/screenshot_2.png) 59 | ![Screenshots 2](https://github.com/triagung128/fic-mini-project/blob/master/assets/screenshots/screenshot_3.png) 60 | ![Screenshots 3](https://github.com/triagung128/fic-mini-project/blob/master/assets/screenshots/screenshot_4.png) 61 | ![Screenshots 4](https://github.com/triagung128/fic-mini-project/blob/master/assets/screenshots/screenshot_5.png) 62 | ![Screenshots 5](https://github.com/triagung128/fic-mini-project/blob/master/assets/screenshots/screenshot_6.png) 63 | -------------------------------------------------------------------------------- /lib/data/models/transaction_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import 'package:fic_mini_project/data/models/cart_model.dart'; 5 | import 'package:fic_mini_project/data/models/user_model.dart'; 6 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 7 | 8 | class TransactionModel extends Equatable { 9 | final UserModel user; 10 | final CartModel cart; 11 | final int usePoint; 12 | final int endTotalPrice; 13 | final String paymentMethod; 14 | final DateTime createdAt; 15 | 16 | const TransactionModel({ 17 | required this.user, 18 | required this.cart, 19 | required this.usePoint, 20 | required this.endTotalPrice, 21 | required this.paymentMethod, 22 | required this.createdAt, 23 | }); 24 | 25 | factory TransactionModel.fromSnapshot(DocumentSnapshot doc) => 26 | TransactionModel( 27 | user: UserModel.fromMap(doc.get('user')), 28 | cart: CartModel.fromMap(doc.get('cart')), 29 | usePoint: doc.get('use_point'), 30 | endTotalPrice: doc.get('end_total_price'), 31 | paymentMethod: doc.get('method_payment'), 32 | createdAt: DateTime.parse(doc.get('created_at')), 33 | ); 34 | 35 | factory TransactionModel.fromEntity(TransactionEntity transaction) => 36 | TransactionModel( 37 | user: UserModel.fromEntity(transaction.user), 38 | cart: CartModel.fromEntity(transaction.cart), 39 | usePoint: transaction.usePoint, 40 | endTotalPrice: transaction.endTotalPrice, 41 | paymentMethod: transaction.paymentMethod, 42 | createdAt: transaction.createdAt, 43 | ); 44 | 45 | Map toDocument() => { 46 | 'user': user.toDocument(), 47 | 'cart': cart.toMap(), 48 | 'use_point': usePoint, 49 | 'end_total_price': endTotalPrice, 50 | 'method_payment': paymentMethod, 51 | 'created_at': createdAt.toIso8601String(), 52 | }; 53 | 54 | TransactionEntity toEntity() => TransactionEntity( 55 | user: user.toEntity(), 56 | cart: cart.toEntity(), 57 | usePoint: usePoint, 58 | endTotalPrice: endTotalPrice, 59 | paymentMethod: paymentMethod, 60 | createdAt: createdAt, 61 | ); 62 | 63 | @override 64 | List get props => [ 65 | user, 66 | cart, 67 | usePoint, 68 | endTotalPrice, 69 | paymentMethod, 70 | createdAt, 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /lib/data/datasources/auth_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:google_sign_in/google_sign_in.dart'; 6 | 7 | import 'package:fic_mini_project/data/models/user_model.dart'; 8 | 9 | abstract class AuthRemoteDataSource { 10 | Future loginWithGoogle(); 11 | bool isLogin(); 12 | Future logout(); 13 | } 14 | 15 | class AuthRemoteDataSourceImpl extends AuthRemoteDataSource { 16 | final GoogleSignIn googleSignIn; 17 | final FirebaseAuth firebaseAuth; 18 | final FirebaseFirestore firebaseFirestore; 19 | 20 | AuthRemoteDataSourceImpl({ 21 | required this.googleSignIn, 22 | required this.firebaseAuth, 23 | required this.firebaseFirestore, 24 | }); 25 | 26 | @override 27 | Future loginWithGoogle() async { 28 | try { 29 | await googleSignIn.disconnect(); 30 | } catch (_) {} 31 | 32 | final GoogleSignInAccount? googleAccount = await googleSignIn.signIn(); 33 | if (googleAccount != null) { 34 | final GoogleSignInAuthentication googleAuth = 35 | await googleAccount.authentication; 36 | final AuthCredential credential = GoogleAuthProvider.credential( 37 | accessToken: googleAuth.accessToken, 38 | idToken: googleAuth.idToken, 39 | ); 40 | 41 | await firebaseAuth.signInWithCredential(credential); 42 | 43 | final currentUser = firebaseAuth.currentUser!; 44 | final userCollection = 45 | firebaseFirestore.collection('users').doc(currentUser.uid); 46 | final user = await userCollection.get(); 47 | if (!user.exists) { 48 | final newUser = UserModel( 49 | id: currentUser.uid, 50 | name: currentUser.displayName, 51 | email: currentUser.email, 52 | phoneNumber: currentUser.phoneNumber, 53 | photoUrl: currentUser.photoURL, 54 | role: 'member', 55 | point: 0, 56 | ); 57 | await userCollection.set(newUser.toDocument()); 58 | } 59 | 60 | final dataUser = await userCollection.get(); 61 | return UserModel.fromSnapshot(dataUser); 62 | } else { 63 | throw PlatformException(code: GoogleSignIn.kSignInCanceledError); 64 | } 65 | } 66 | 67 | @override 68 | bool isLogin() { 69 | return firebaseAuth.currentUser != null; 70 | } 71 | 72 | @override 73 | Future logout() async { 74 | return await firebaseAuth.signOut(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/presentation/blocs/product/product_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/domain/usecases/product/get_all_products.dart'; 6 | import 'package:fic_mini_project/domain/usecases/product/insert_product.dart'; 7 | import 'package:fic_mini_project/domain/usecases/product/remove_product.dart'; 8 | import 'package:fic_mini_project/domain/usecases/product/update_product.dart'; 9 | 10 | part 'product_event.dart'; 11 | part 'product_state.dart'; 12 | 13 | class ProductBloc extends Bloc { 14 | final GetAllProducts getAllProducts; 15 | final InsertProduct insertProduct; 16 | final UpdateProduct updateProduct; 17 | final RemoveProduct removeProduct; 18 | 19 | ProductBloc({ 20 | required this.getAllProducts, 21 | required this.insertProduct, 22 | required this.updateProduct, 23 | required this.removeProduct, 24 | }) : super(ProductInitial()) { 25 | on((_, emit) async { 26 | emit(ProductLoading()); 27 | 28 | final result = await getAllProducts.execute(); 29 | 30 | result.fold( 31 | (failure) => emit(ProductFailure(failure.message)), 32 | (data) { 33 | if (data.isEmpty) { 34 | emit(ProductEmpty()); 35 | } else { 36 | emit(AllProductsLoaded(data)); 37 | } 38 | }, 39 | ); 40 | }); 41 | 42 | on((event, emit) async { 43 | emit(ProductLoading()); 44 | 45 | final result = await insertProduct.execute(event.product); 46 | 47 | result.fold( 48 | (failure) => emit(ProductActionFailure(failure.message)), 49 | (successMessage) => emit(ProductActionSuccess(successMessage)), 50 | ); 51 | 52 | add(OnFetchAllProducts()); 53 | }); 54 | 55 | on((event, emit) async { 56 | emit(ProductLoading()); 57 | 58 | final result = await updateProduct.execute(event.product); 59 | 60 | result.fold( 61 | (failure) => emit(ProductActionFailure(failure.message)), 62 | (successMessage) => emit(ProductActionSuccess(successMessage)), 63 | ); 64 | 65 | add(OnFetchAllProducts()); 66 | }); 67 | 68 | on((event, emit) async { 69 | emit(ProductLoading()); 70 | 71 | final result = await removeProduct.execute(event.product); 72 | 73 | result.fold( 74 | (failure) => emit(ProductActionFailure(failure.message)), 75 | (successMessage) => emit(ProductActionSuccess(successMessage)), 76 | ); 77 | 78 | add(OnFetchAllProducts()); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/presentation/blocs/category/category_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/domain/entity/category.dart'; 5 | import 'package:fic_mini_project/domain/usecases/category/get_all_categories.dart'; 6 | import 'package:fic_mini_project/domain/usecases/category/insert_category.dart'; 7 | import 'package:fic_mini_project/domain/usecases/category/remove_category.dart'; 8 | import 'package:fic_mini_project/domain/usecases/category/update_category.dart'; 9 | 10 | part 'category_event.dart'; 11 | part 'category_state.dart'; 12 | 13 | class CategoryBloc extends Bloc { 14 | final GetAllCategories getAllCategories; 15 | final InsertCategory insertCategory; 16 | final UpdateCategory updateCategory; 17 | final RemoveCategory removeCategory; 18 | 19 | CategoryBloc({ 20 | required this.getAllCategories, 21 | required this.insertCategory, 22 | required this.updateCategory, 23 | required this.removeCategory, 24 | }) : super(CategoryInitial()) { 25 | on((event, emit) async { 26 | emit(CategoryLoading()); 27 | 28 | final result = await getAllCategories.execute(); 29 | 30 | result.fold( 31 | (failure) => emit(CategoryFailure(failure.message)), 32 | (data) { 33 | if (data.isEmpty) { 34 | emit(CategoryEmpty()); 35 | } else { 36 | emit(AllCategoriesLoaded(data)); 37 | } 38 | }, 39 | ); 40 | }); 41 | 42 | on((event, emit) async { 43 | emit(CategoryLoading()); 44 | 45 | final result = await insertCategory.execute(event.category); 46 | 47 | result.fold( 48 | (failure) => emit(CategoryActionFailure(failure.message)), 49 | (successMessage) => emit(CategoryActionSuccess(successMessage)), 50 | ); 51 | 52 | add(OnFetchAllCategories()); 53 | }); 54 | 55 | on((event, emit) async { 56 | emit(CategoryLoading()); 57 | 58 | final result = await updateCategory.execute(event.category); 59 | 60 | result.fold( 61 | (failure) => emit(CategoryActionFailure(failure.message)), 62 | (successMessage) => emit(CategoryActionSuccess(successMessage)), 63 | ); 64 | 65 | add(OnFetchAllCategories()); 66 | }); 67 | 68 | on((event, emit) async { 69 | emit(CategoryLoading()); 70 | 71 | final result = await removeCategory.execute(event.category); 72 | 73 | result.fold( 74 | (failure) => emit(CategoryActionFailure(failure.message)), 75 | (successMessage) => emit(CategoryActionSuccess(successMessage)), 76 | ); 77 | 78 | add(OnFetchAllCategories()); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/data/datasources/point_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | 4 | import 'package:fic_mini_project/data/models/point_model.dart'; 5 | import 'package:fic_mini_project/data/models/user_model.dart'; 6 | 7 | abstract class PointRemoteDataSource { 8 | Future> getAllPointsHistory(); 9 | Future savePoint(int point); 10 | Future usePoint(int point); 11 | } 12 | 13 | class PointRemoteDataSourceImpl extends PointRemoteDataSource { 14 | final FirebaseAuth firebaseAuth; 15 | final FirebaseFirestore firebaseFirestore; 16 | 17 | PointRemoteDataSourceImpl({ 18 | required this.firebaseAuth, 19 | required this.firebaseFirestore, 20 | }); 21 | 22 | @override 23 | Future> getAllPointsHistory() async { 24 | final userId = firebaseAuth.currentUser!.uid; 25 | 26 | final pointCollection = await firebaseFirestore 27 | .collection('points_history') 28 | .where('user_id', isEqualTo: userId) 29 | .orderBy('created_at', descending: true) 30 | .get(); 31 | 32 | final docs = pointCollection.docs; 33 | if (docs.isEmpty) { 34 | return []; 35 | } else { 36 | return docs.map((e) => PointModel.fromSnapshot(e)).toList(); 37 | } 38 | } 39 | 40 | @override 41 | Future savePoint(int point) async { 42 | final userId = firebaseAuth.currentUser!.uid; 43 | 44 | final newPoint = PointModel( 45 | userId: userId, 46 | point: point, 47 | isEntry: true, 48 | createdAt: DateTime.now(), 49 | ); 50 | 51 | await firebaseFirestore 52 | .collection('users') 53 | .doc(userId) 54 | .collection('point') 55 | .add(newPoint.toDocument()); 56 | 57 | final user = await firebaseFirestore.collection('users').doc(userId).get(); 58 | final pointNow = UserModel.fromSnapshot(user).point; 59 | await firebaseFirestore.collection('users').doc(userId).update( 60 | { 61 | 'point': pointNow! + point, 62 | }, 63 | ); 64 | } 65 | 66 | @override 67 | Future usePoint(int point) async { 68 | final userId = firebaseAuth.currentUser!.uid; 69 | 70 | final newPoint = PointModel( 71 | userId: userId, 72 | point: point, 73 | isEntry: false, 74 | createdAt: DateTime.now(), 75 | ); 76 | 77 | await firebaseFirestore 78 | .collection('users') 79 | .doc(userId) 80 | .collection('point') 81 | .add(newPoint.toDocument()); 82 | 83 | final user = await firebaseFirestore.collection('users').doc(userId).get(); 84 | final pointNow = UserModel.fromSnapshot(user).point; 85 | await firebaseFirestore.collection('users').doc(userId).update( 86 | { 87 | 'point': pointNow! - point, 88 | }, 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/presentation/blocs/pos/pos_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import 'package:fic_mini_project/common/enum_state.dart'; 5 | import 'package:fic_mini_project/domain/entity/cart.dart'; 6 | import 'package:fic_mini_project/domain/entity/product.dart'; 7 | import 'package:fic_mini_project/domain/usecases/cart/add_product_quantity.dart'; 8 | import 'package:fic_mini_project/domain/usecases/cart/add_product_to_cart.dart'; 9 | import 'package:fic_mini_project/domain/usecases/cart/clear_cart.dart'; 10 | import 'package:fic_mini_project/domain/usecases/cart/get_all_carts_map.dart'; 11 | import 'package:fic_mini_project/domain/usecases/cart/reduce_product_quantity.dart'; 12 | 13 | part 'pos_event.dart'; 14 | part 'pos_state.dart'; 15 | 16 | class PosBloc extends Bloc { 17 | final AddProductToCart addProductToCart; 18 | final AddProductQuantity addProductQuantity; 19 | final ReduceProductQuantity reduceProductQuantity; 20 | final ClearCart clearCart; 21 | final GetAllCartsMap getAllCartsMap; 22 | 23 | PosBloc({ 24 | required this.addProductToCart, 25 | required this.addProductQuantity, 26 | required this.reduceProductQuantity, 27 | required this.clearCart, 28 | required this.getAllCartsMap, 29 | }) : super(PosState.initial()) { 30 | on((event, emit) async { 31 | await clearCart.execute(); 32 | emit(PosState.initial()); 33 | }); 34 | 35 | on((event, emit) async { 36 | final result = await addProductToCart.execute(event.product); 37 | emit( 38 | state.copyWith( 39 | cart: result, 40 | actionState: PosActionState.noAction, 41 | ), 42 | ); 43 | }); 44 | 45 | on((event, emit) async { 46 | final result = await addProductQuantity.execute(event.product); 47 | emit( 48 | state.copyWith( 49 | cart: result, 50 | actionState: PosActionState.noAction, 51 | ), 52 | ); 53 | }); 54 | 55 | on((event, emit) async { 56 | final result = await reduceProductQuantity.execute(event.product); 57 | emit( 58 | state.copyWith( 59 | cart: result, 60 | actionState: PosActionState.noAction, 61 | ), 62 | ); 63 | }); 64 | 65 | on((event, emit) async { 66 | final result = await getAllCartsMap.execute(); 67 | if (result.containsValue(0)) { 68 | emit( 69 | state.copyWith( 70 | actionState: PosActionState.failed, 71 | ), 72 | ); 73 | } else { 74 | emit( 75 | state.copyWith( 76 | actionState: PosActionState.success, 77 | cartMap: result, 78 | ), 79 | ); 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply plugin: 'com.google.gms.google-services' 27 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 28 | 29 | def keystoreProperties = new Properties() 30 | def keystorePropertiesFile = rootProject.file('key.properties') 31 | if (keystorePropertiesFile.exists()) { 32 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 33 | } 34 | 35 | android { 36 | compileSdkVersion 33 37 | ndkVersion flutter.ndkVersion 38 | 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | 44 | kotlinOptions { 45 | jvmTarget = '1.8' 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | } 51 | 52 | defaultConfig { 53 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 54 | applicationId "com.triagung.fic_mini_project" 55 | // You can update the following values to match your application needs. 56 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 57 | minSdkVersion 21 58 | multiDexEnabled true 59 | targetSdkVersion 33 60 | versionCode flutterVersionCode.toInteger() 61 | versionName flutterVersionName 62 | } 63 | 64 | signingConfigs { 65 | release { 66 | keyAlias keystoreProperties['keyAlias'] 67 | keyPassword keystoreProperties['keyPassword'] 68 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 69 | storePassword keystoreProperties['storePassword'] 70 | } 71 | } 72 | 73 | buildTypes { 74 | release { 75 | signingConfig signingConfigs.release 76 | } 77 | } 78 | } 79 | 80 | flutter { 81 | source '../..' 82 | } 83 | 84 | dependencies { 85 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 86 | } 87 | -------------------------------------------------------------------------------- /lib/data/datasources/transaction_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | 4 | import 'package:fic_mini_project/data/models/point_model.dart'; 5 | import 'package:fic_mini_project/data/models/transaction_model.dart'; 6 | import 'package:fic_mini_project/data/models/user_model.dart'; 7 | 8 | abstract class TransactionRemoteDataSource { 9 | Future> getAllTransactions(); 10 | Future> getAllTransactionsByUserId(); 11 | Future saveTransaction(TransactionModel transaction); 12 | } 13 | 14 | class TransactionRemoteDataSourceImpl extends TransactionRemoteDataSource { 15 | FirebaseAuth firebaseAuth; 16 | FirebaseFirestore firebaseFirestore; 17 | 18 | TransactionRemoteDataSourceImpl({ 19 | required this.firebaseAuth, 20 | required this.firebaseFirestore, 21 | }); 22 | 23 | @override 24 | Future> getAllTransactions() async { 25 | final transactionCollection = await firebaseFirestore 26 | .collection('transactions') 27 | .orderBy('created_at', descending: true) 28 | .get(); 29 | 30 | final docs = transactionCollection.docs; 31 | if (docs.isEmpty) { 32 | return []; 33 | } else { 34 | return docs.map((doc) => TransactionModel.fromSnapshot(doc)).toList(); 35 | } 36 | } 37 | 38 | @override 39 | Future> getAllTransactionsByUserId() async { 40 | final userId = firebaseAuth.currentUser!.uid; 41 | 42 | final transactionCollection = await firebaseFirestore 43 | .collection('transactions') 44 | .orderBy('created_at', descending: true) 45 | .where('user.id', isEqualTo: userId) 46 | .get(); 47 | 48 | final docs = transactionCollection.docs; 49 | if (docs.isEmpty) { 50 | return []; 51 | } else { 52 | return docs.map((doc) => TransactionModel.fromSnapshot(doc)).toList(); 53 | } 54 | } 55 | 56 | @override 57 | Future saveTransaction(TransactionModel transaction) async { 58 | final userId = firebaseAuth.currentUser!.uid; 59 | 60 | final cashbackPoints = (transaction.cart.totalPrice * 0.1).toInt(); 61 | 62 | final addNewPoint = PointModel( 63 | userId: userId, 64 | point: cashbackPoints, 65 | isEntry: true, 66 | createdAt: DateTime.now(), 67 | ); 68 | 69 | await firebaseFirestore 70 | .collection('points_history') 71 | .add(addNewPoint.toDocument()); 72 | 73 | if (transaction.usePoint > 0) { 74 | final decreasePoint = PointModel( 75 | userId: userId, 76 | point: transaction.usePoint, 77 | isEntry: false, 78 | createdAt: DateTime.now(), 79 | ); 80 | 81 | await firebaseFirestore 82 | .collection('points_history') 83 | .add(decreasePoint.toDocument()); 84 | } 85 | 86 | final user = await firebaseFirestore.collection('users').doc(userId).get(); 87 | final pointNow = UserModel.fromSnapshot(user).point; 88 | 89 | await firebaseFirestore 90 | .collection('users') 91 | .doc(userId) 92 | .update({'point': (pointNow! - transaction.usePoint) + cashbackPoints}); 93 | 94 | await firebaseFirestore 95 | .collection('transactions') 96 | .add(transaction.toDocument()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/data/db/database_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | 3 | import 'package:fic_mini_project/data/models/category_model.dart'; 4 | import 'package:fic_mini_project/data/models/product_model.dart'; 5 | 6 | class DatabaseHelper { 7 | static DatabaseHelper? _databaseHelper; 8 | 9 | DatabaseHelper._instance() { 10 | _databaseHelper = this; 11 | } 12 | 13 | factory DatabaseHelper() => _databaseHelper ?? DatabaseHelper._instance(); 14 | 15 | static Database? _database; 16 | 17 | Future get database async { 18 | _database ??= await _initDb(); 19 | return _database; 20 | } 21 | 22 | Future _initDb() async { 23 | final path = await getDatabasesPath(); 24 | final databasePath = '$path/fic_mini_project.db'; 25 | 26 | final db = await openDatabase( 27 | databasePath, 28 | version: 1, 29 | onCreate: _onCreate, 30 | ); 31 | return db; 32 | } 33 | 34 | static const String _tblCategory = 'tb_category'; 35 | static const String _tblProduct = 'tb_product'; 36 | 37 | void _onCreate(Database db, int version) async { 38 | await db.execute(''' 39 | CREATE TABLE $_tblCategory ( 40 | id INTEGER PRIMARY KEY, 41 | name TEXT 42 | ); 43 | '''); 44 | 45 | await db.execute(''' 46 | CREATE TABLE $_tblProduct ( 47 | id INTEGER PRIMARY KEY, 48 | name TEXT, 49 | price INTEGER, 50 | category_id INTEGER, 51 | image BLOB, 52 | FOREIGN KEY (category_id) REFERENCES $_tblCategory (id) 53 | ); 54 | '''); 55 | } 56 | 57 | Future>> getAllCategories() async { 58 | final db = await database; 59 | return await db!.query( 60 | _tblCategory, 61 | orderBy: 'id DESC', 62 | ); 63 | } 64 | 65 | Future insertCategory(CategoryModel category) async { 66 | final db = await database; 67 | return await db!.insert(_tblCategory, category.toMap()); 68 | } 69 | 70 | Future updateCategory(CategoryModel category) async { 71 | final db = await database; 72 | return await db!.update( 73 | _tblCategory, 74 | category.toMap(), 75 | where: 'id = ?', 76 | whereArgs: [category.id], 77 | ); 78 | } 79 | 80 | Future removeCategory(CategoryModel category) async { 81 | final db = await database; 82 | return await db!.delete( 83 | _tblCategory, 84 | where: 'id = ?', 85 | whereArgs: [category.id], 86 | ); 87 | } 88 | 89 | Future>> getAllProducts() async { 90 | final db = await database; 91 | return await db!.rawQuery( 92 | ''' 93 | SELECT $_tblProduct.*, $_tblCategory.name AS category_name 94 | FROM $_tblProduct 95 | INNER JOIN $_tblCategory 96 | ON $_tblProduct.category_id = $_tblCategory.id 97 | ORDER BY id DESC 98 | ''', 99 | ); 100 | } 101 | 102 | Future insertProduct(ProductModel product) async { 103 | final db = await database; 104 | return await db!.insert(_tblProduct, product.toMap()); 105 | } 106 | 107 | Future updateProduct(ProductModel product) async { 108 | final db = await database; 109 | return await db!.update( 110 | _tblProduct, 111 | product.toMap(), 112 | where: 'id = ?', 113 | whereArgs: [product.id], 114 | ); 115 | } 116 | 117 | Future removeProduct(ProductModel product) async { 118 | final db = await database; 119 | return await db!.delete( 120 | _tblProduct, 121 | where: 'id = ?', 122 | whereArgs: [product.id], 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/presentation/pages/member/member_transaction_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import 'package:fic_mini_project/common/currency_rupiah_extension.dart'; 6 | import 'package:fic_mini_project/common/datetime_extension.dart'; 7 | import 'package:fic_mini_project/common/styles.dart'; 8 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 9 | import 'package:fic_mini_project/presentation/blocs/transaction/transaction_bloc.dart'; 10 | 11 | class MemberTransactionPage extends StatefulWidget { 12 | const MemberTransactionPage({super.key}); 13 | 14 | @override 15 | State createState() => _MemberTransactionPageState(); 16 | } 17 | 18 | class _MemberTransactionPageState extends State { 19 | @override 20 | void initState() { 21 | super.initState(); 22 | context.read().add(OnFetchAllTransactionsByUserId()); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | backgroundColor: greyColor, 29 | appBar: AppBar( 30 | title: const Text('Riwayat Transaksi'), 31 | ), 32 | body: BlocBuilder( 33 | builder: (context, state) { 34 | if (state is TransactionLoading) { 35 | return const Center( 36 | child: CircularProgressIndicator(), 37 | ); 38 | } else if (state is AllTransactionsLoaded) { 39 | return ListView.builder( 40 | padding: const EdgeInsets.only( 41 | left: 16, 42 | right: 16, 43 | top: 16, 44 | ), 45 | itemCount: state.transactions.length, 46 | itemBuilder: (_, index) { 47 | final transaction = state.transactions[index]; 48 | return _MemberTransactionCard(transaction: transaction); 49 | }, 50 | ); 51 | } else if (state is TransactionEmpty) { 52 | return const Center( 53 | child: Text('Data Kosong'), 54 | ); 55 | } else if (state is TransactionFailure) { 56 | return Center( 57 | child: Text(state.message), 58 | ); 59 | } else { 60 | return Container(); 61 | } 62 | }, 63 | ), 64 | ); 65 | } 66 | } 67 | 68 | class _MemberTransactionCard extends StatelessWidget { 69 | const _MemberTransactionCard({ 70 | required this.transaction, 71 | }); 72 | 73 | final TransactionEntity transaction; 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return Container( 78 | margin: const EdgeInsets.only(bottom: 16), 79 | padding: const EdgeInsets.all(16), 80 | decoration: BoxDecoration( 81 | color: whiteColor, 82 | borderRadius: BorderRadius.circular(16), 83 | ), 84 | child: Column( 85 | crossAxisAlignment: CrossAxisAlignment.start, 86 | children: [ 87 | Row( 88 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 89 | children: [ 90 | Text( 91 | transaction.createdAt.dateTimeFormatter, 92 | style: Theme.of(context).textTheme.bodyLarge, 93 | ), 94 | Row( 95 | children: [ 96 | const Icon( 97 | Icons.check_circle_outline_rounded, 98 | color: Colors.green, 99 | ), 100 | const SizedBox(width: 4), 101 | Text( 102 | 'Selesai', 103 | style: Theme.of(context) 104 | .textTheme 105 | .bodyMedium! 106 | .copyWith(color: Colors.green), 107 | ), 108 | ], 109 | ), 110 | ], 111 | ), 112 | const Padding( 113 | padding: EdgeInsets.symmetric(vertical: 16), 114 | child: Divider(thickness: 0.2, height: 1), 115 | ), 116 | const Text('Total :'), 117 | const SizedBox(height: 4), 118 | Text( 119 | transaction.endTotalPrice.intToFormatRupiah, 120 | style: Theme.of(context) 121 | .textTheme 122 | .bodyLarge! 123 | .copyWith(color: blueColor), 124 | ), 125 | ], 126 | ), 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:intl/date_symbol_data_local.dart'; 7 | 8 | import 'package:fic_mini_project/common/routes.dart'; 9 | import 'package:fic_mini_project/common/styles.dart'; 10 | import 'package:fic_mini_project/firebase_options.dart'; 11 | import 'package:fic_mini_project/injection.dart' as di; 12 | import 'package:fic_mini_project/presentation/blocs/auth/auth_bloc.dart'; 13 | import 'package:fic_mini_project/presentation/blocs/category/category_bloc.dart'; 14 | import 'package:fic_mini_project/presentation/blocs/point/point_bloc.dart'; 15 | import 'package:fic_mini_project/presentation/blocs/pos/pos_bloc.dart'; 16 | import 'package:fic_mini_project/presentation/blocs/product/product_bloc.dart'; 17 | import 'package:fic_mini_project/presentation/blocs/profile/profile_bloc.dart'; 18 | import 'package:fic_mini_project/presentation/blocs/report/report_bloc.dart'; 19 | import 'package:fic_mini_project/presentation/blocs/transaction/transaction_bloc.dart'; 20 | 21 | void main() async { 22 | WidgetsFlutterBinding.ensureInitialized(); 23 | 24 | await initializeDateFormatting('id_ID', null); 25 | 26 | di.init(); 27 | 28 | await Firebase.initializeApp( 29 | options: DefaultFirebaseOptions.currentPlatform, 30 | ); 31 | 32 | runApp(const MyApp()); 33 | } 34 | 35 | class MyApp extends StatelessWidget { 36 | const MyApp({super.key}); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return MultiBlocProvider( 41 | providers: [ 42 | BlocProvider( 43 | create: (_) => di.locator(), 44 | ), 45 | BlocProvider( 46 | create: (_) => di.locator(), 47 | ), 48 | BlocProvider( 49 | create: (_) => di.locator(), 50 | ), 51 | BlocProvider( 52 | create: (_) => di.locator(), 53 | ), 54 | BlocProvider( 55 | create: (_) => di.locator(), 56 | ), 57 | BlocProvider( 58 | create: (_) => di.locator(), 59 | ), 60 | BlocProvider( 61 | create: (_) => di.locator(), 62 | ), 63 | BlocProvider( 64 | create: (_) => di.locator(), 65 | ), 66 | ], 67 | child: MaterialApp( 68 | title: 'Palem Kafe POS App', 69 | debugShowCheckedModeBanner: false, 70 | theme: ThemeData( 71 | useMaterial3: false, 72 | scaffoldBackgroundColor: whiteColor, 73 | visualDensity: VisualDensity.adaptivePlatformDensity, 74 | textTheme: textTheme, 75 | colorScheme: Theme.of(context).colorScheme.copyWith( 76 | primary: blueColor, 77 | secondary: navyColor, 78 | ), 79 | appBarTheme: Theme.of(context).appBarTheme.copyWith( 80 | systemOverlayStyle: const SystemUiOverlayStyle( 81 | statusBarColor: whiteColor, 82 | statusBarIconBrightness: Brightness.dark, 83 | ), 84 | elevation: 0, 85 | backgroundColor: whiteColor, 86 | centerTitle: true, 87 | iconTheme: const IconThemeData(color: blueColor), 88 | titleTextStyle: Theme.of(context) 89 | .textTheme 90 | .titleLarge! 91 | .copyWith(color: blueColor, fontWeight: FontWeight.w700), 92 | ), 93 | inputDecorationTheme: Theme.of(context).inputDecorationTheme.copyWith( 94 | contentPadding: const EdgeInsets.symmetric( 95 | horizontal: 24, 96 | vertical: 21, 97 | ), 98 | border: OutlineInputBorder( 99 | borderRadius: BorderRadius.circular(16), 100 | ), 101 | enabledBorder: OutlineInputBorder( 102 | borderRadius: BorderRadius.circular(16), 103 | borderSide: BorderSide.none, 104 | ), 105 | filled: true, 106 | fillColor: Colors.transparent.withOpacity(0.05), 107 | hintStyle: Theme.of(context) 108 | .textTheme 109 | .bodyMedium! 110 | .copyWith(color: navyColor.withOpacity(0.5)), 111 | ), 112 | ), 113 | initialRoute: splashRoute, 114 | onGenerateRoute: Routes.generateRoute, 115 | ), 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/presentation/pages/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 5 | 6 | import 'package:fic_mini_project/common/routes.dart'; 7 | import 'package:fic_mini_project/common/styles.dart'; 8 | import 'package:fic_mini_project/presentation/blocs/auth/auth_bloc.dart'; 9 | 10 | class LoginPage extends StatelessWidget { 11 | const LoginPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Log in'), 18 | ), 19 | body: BlocConsumer( 20 | listener: (context, state) { 21 | if (state is LoginAsVendorAuthenticated) { 22 | Navigator.pushNamedAndRemoveUntil( 23 | context, vendorHomeRoute, (_) => false); 24 | } else if (state is LoginAsMemberAuthenticated) { 25 | Navigator.pushNamedAndRemoveUntil( 26 | context, memberHomeRoute, (_) => false); 27 | } else if (state is AuthFailure) { 28 | if (state.message != '') { 29 | ScaffoldMessenger.of(context) 30 | ..hideCurrentSnackBar() 31 | ..showSnackBar( 32 | SnackBar( 33 | content: Text(state.message), 34 | backgroundColor: Colors.red, 35 | ), 36 | ); 37 | } 38 | } 39 | }, 40 | builder: (context, state) { 41 | return Container( 42 | padding: const EdgeInsets.only( 43 | top: 40, 44 | left: 24, 45 | right: 24, 46 | ), 47 | child: SingleChildScrollView( 48 | child: Column( 49 | crossAxisAlignment: CrossAxisAlignment.start, 50 | children: [ 51 | Text( 52 | 'Palem Kafe POS App', 53 | style: Theme.of(context).textTheme.headlineSmall!.copyWith( 54 | color: navyColor, 55 | fontWeight: FontWeight.w700, 56 | ), 57 | ), 58 | const SizedBox(height: 8), 59 | const Text( 60 | 'Silahkan login terlebih dahulu sebagai vendor atau member untuk melanjutkan.', 61 | ), 62 | const SizedBox(height: 80), 63 | Center( 64 | child: Container( 65 | width: 180, 66 | height: 180, 67 | decoration: BoxDecoration( 68 | image: const DecorationImage( 69 | image: AssetImage('assets/logo/logo.png'), 70 | ), 71 | borderRadius: BorderRadius.circular(16), 72 | ), 73 | ), 74 | ), 75 | const SizedBox(height: 80), 76 | _LoginButton( 77 | onPressed: state is AuthLoading 78 | ? null 79 | : () => context.read().add(LoginSubmitted()), 80 | text: 'Log in by Google Account', 81 | ), 82 | state is AuthLoading 83 | ? Container( 84 | margin: const EdgeInsets.only(top: 24), 85 | child: const Center( 86 | child: CircularProgressIndicator(), 87 | ), 88 | ) 89 | : const SizedBox(), 90 | ], 91 | ), 92 | ), 93 | ); 94 | }, 95 | ), 96 | ); 97 | } 98 | } 99 | 100 | class _LoginButton extends StatelessWidget { 101 | const _LoginButton({ 102 | required this.onPressed, 103 | required this.text, 104 | }); 105 | 106 | final Function()? onPressed; 107 | final String text; 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | return ElevatedButton( 112 | onPressed: onPressed, 113 | style: ElevatedButton.styleFrom( 114 | fixedSize: Size(MediaQuery.of(context).size.width, 57), 115 | shape: RoundedRectangleBorder( 116 | borderRadius: BorderRadius.circular(16), 117 | ), 118 | ), 119 | child: Stack( 120 | children: [ 121 | const Align( 122 | alignment: Alignment.centerLeft, 123 | child: FaIcon(FontAwesomeIcons.google), 124 | ), 125 | Align( 126 | alignment: Alignment.center, 127 | child: Text(text), 128 | ), 129 | ], 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/presentation/pages/member/member_point_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import 'package:fic_mini_project/common/datetime_extension.dart'; 6 | import 'package:fic_mini_project/common/point_extension.dart'; 7 | import 'package:fic_mini_project/common/styles.dart'; 8 | import 'package:fic_mini_project/domain/entity/point.dart'; 9 | import 'package:fic_mini_project/presentation/blocs/point/point_bloc.dart'; 10 | 11 | class MemberPointPage extends StatefulWidget { 12 | const MemberPointPage({super.key}); 13 | 14 | @override 15 | State createState() => _MemberPointPageState(); 16 | } 17 | 18 | class _MemberPointPageState extends State 19 | with SingleTickerProviderStateMixin { 20 | late TabController _tabController; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | 26 | _tabController = TabController( 27 | length: 2, 28 | vsync: this, 29 | initialIndex: 0, 30 | ); 31 | 32 | Future.microtask( 33 | () => context.read().add(OnFetchAllPointsHistory()), 34 | ); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | appBar: AppBar( 41 | title: const Text("Riwayat Points"), 42 | ), 43 | body: Column( 44 | children: [ 45 | TabBar( 46 | controller: _tabController, 47 | labelColor: navyColor, 48 | labelStyle: Theme.of(context).textTheme.titleMedium, 49 | tabs: [ 50 | Tab( 51 | text: 'Point Masuk', 52 | icon: Icon( 53 | Icons.arrow_downward, 54 | size: 18, 55 | color: Colors.green[600], 56 | ), 57 | ), 58 | Tab( 59 | text: 'Point Keluar', 60 | icon: Icon( 61 | Icons.arrow_upward, 62 | size: 18, 63 | color: Colors.red[600], 64 | ), 65 | ), 66 | ], 67 | ), 68 | Expanded( 69 | child: BlocBuilder( 70 | builder: (context, state) { 71 | if (state is PointLoading) { 72 | return const Center( 73 | child: CircularProgressIndicator(), 74 | ); 75 | } else if (state is AllPointsLoaded) { 76 | final entryPoints = state.points 77 | .where((item) => item.isEntry == true) 78 | .toList(); 79 | 80 | final exitPoints = state.points 81 | .where((item) => item.isEntry == false) 82 | .toList(); 83 | 84 | return TabBarView( 85 | controller: _tabController, 86 | children: [ 87 | _ListViewPoint(points: entryPoints), 88 | _ListViewPoint(points: exitPoints), 89 | ], 90 | ); 91 | } else if (state is PointFailure) { 92 | return Center( 93 | child: Text(state.message), 94 | ); 95 | } else { 96 | return Container(); 97 | } 98 | }, 99 | ), 100 | ), 101 | ], 102 | ), 103 | ); 104 | } 105 | 106 | @override 107 | void dispose() { 108 | super.dispose(); 109 | _tabController.dispose(); 110 | } 111 | } 112 | 113 | class _ListViewPoint extends StatelessWidget { 114 | const _ListViewPoint({ 115 | required this.points, 116 | }); 117 | 118 | final List points; 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | return points.isEmpty 123 | ? const Center( 124 | child: Text('Data Kosong'), 125 | ) 126 | : ListView.separated( 127 | separatorBuilder: (_, __) => const Divider( 128 | thickness: 0.2, 129 | height: 1, 130 | ), 131 | itemCount: points.length, 132 | itemBuilder: (context, index) { 133 | final point = points[index]; 134 | return ListTile( 135 | title: Text(point.createdAt.dateTimeFormatter), 136 | trailing: Text( 137 | points.first.isEntry == true 138 | ? '+${point.point.pointFormatter} Points' 139 | : '-${point.point.pointFormatter} Points', 140 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( 141 | color: points.first.isEntry == true 142 | ? Colors.green[600] 143 | : Colors.red[600], 144 | ), 145 | ), 146 | ); 147 | }, 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/common/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:fic_mini_project/domain/entity/cart.dart'; 4 | import 'package:fic_mini_project/domain/entity/product.dart'; 5 | import 'package:fic_mini_project/presentation/pages/login_page.dart'; 6 | import 'package:fic_mini_project/presentation/pages/member/member_checkout_page.dart'; 7 | import 'package:fic_mini_project/presentation/pages/member/member_home_page.dart'; 8 | import 'package:fic_mini_project/presentation/pages/member/member_point_page.dart'; 9 | import 'package:fic_mini_project/presentation/pages/member/member_transaction_page.dart'; 10 | import 'package:fic_mini_project/presentation/pages/member/order_success_page.dart'; 11 | import 'package:fic_mini_project/presentation/pages/member/scan_qr_code_page.dart'; 12 | import 'package:fic_mini_project/presentation/pages/profile/profile_page.dart'; 13 | import 'package:fic_mini_project/presentation/pages/profile/update_profile_page.dart'; 14 | import 'package:fic_mini_project/presentation/pages/splash_page.dart'; 15 | import 'package:fic_mini_project/presentation/pages/vendor/category_page.dart'; 16 | import 'package:fic_mini_project/presentation/pages/vendor/pos_checkout_page.dart'; 17 | import 'package:fic_mini_project/presentation/pages/vendor/pos_page.dart'; 18 | import 'package:fic_mini_project/presentation/pages/vendor/product_add_update_page.dart'; 19 | import 'package:fic_mini_project/presentation/pages/vendor/product_page.dart'; 20 | import 'package:fic_mini_project/presentation/pages/vendor/transaction_page.dart'; 21 | import 'package:fic_mini_project/presentation/pages/vendor/vendor_home_page.dart'; 22 | 23 | const String splashRoute = '/'; 24 | const String loginRoute = '/login'; 25 | const String memberHomeRoute = '/member-home'; 26 | const String vendorHomeRoute = '/vendor-home'; 27 | const String profileRoute = '/profile'; 28 | const String updateProfileRoute = '/update-profile'; 29 | const String categoryRoute = '/category'; 30 | const String productRoute = '/product'; 31 | const String productAddUpdateRoute = '/product-add-update'; 32 | const String posRoute = '/pos'; 33 | const String posCheckoutRoute = '/pos-checkout'; 34 | const String transactionRoute = '/transaction'; 35 | const String scanQRCodeRoute = '/scan-qr-code'; 36 | const String memberCheckoutRoute = '/member-checkout'; 37 | const String orderSuccessRoute = '/order-success'; 38 | const String memberPointPageRoute = '/member-point'; 39 | const String memberTransactionRoute = '/member-transaction'; 40 | 41 | class Routes { 42 | static Route generateRoute(RouteSettings settings) { 43 | switch (settings.name) { 44 | case splashRoute: 45 | return MaterialPageRoute(builder: (_) => const SplashPage()); 46 | case loginRoute: 47 | return MaterialPageRoute(builder: (_) => const LoginPage()); 48 | case memberHomeRoute: 49 | return MaterialPageRoute(builder: (_) => const MemberHomePage()); 50 | case vendorHomeRoute: 51 | return MaterialPageRoute(builder: (_) => const VendorHomePage()); 52 | case profileRoute: 53 | return MaterialPageRoute(builder: (_) => const ProfilePage()); 54 | case updateProfileRoute: 55 | return MaterialPageRoute(builder: (_) => const UpdateProfilePage()); 56 | case categoryRoute: 57 | return MaterialPageRoute(builder: (_) => const CategoryPage()); 58 | case productRoute: 59 | return MaterialPageRoute(builder: (_) => const ProductPage()); 60 | case posRoute: 61 | return MaterialPageRoute(builder: (_) => const PosPage()); 62 | case posCheckoutRoute: 63 | final args = settings.arguments as Map; 64 | return MaterialPageRoute( 65 | builder: (_) => PosCheckoutPage( 66 | cartMap: args['cartMap'], 67 | cart: args['cart'], 68 | ), 69 | ); 70 | case transactionRoute: 71 | return MaterialPageRoute(builder: (_) => const TransactionPage()); 72 | case scanQRCodeRoute: 73 | return MaterialPageRoute(builder: (_) => const ScanQrCodePage()); 74 | case memberCheckoutRoute: 75 | final cart = settings.arguments as Cart; 76 | return MaterialPageRoute( 77 | builder: (_) => MemberCheckoutPage(cart: cart), 78 | settings: settings, 79 | ); 80 | case productAddUpdateRoute: 81 | final product = settings.arguments as Product?; 82 | return MaterialPageRoute( 83 | builder: (_) => ProductAddUpdatePage(product: product), 84 | settings: settings, 85 | ); 86 | case orderSuccessRoute: 87 | return MaterialPageRoute(builder: (_) => const OrderSuccessPage()); 88 | case memberPointPageRoute: 89 | return MaterialPageRoute(builder: (_) => const MemberPointPage()); 90 | case memberTransactionRoute: 91 | return MaterialPageRoute( 92 | builder: (_) => const MemberTransactionPage(), 93 | ); 94 | default: 95 | return MaterialPageRoute( 96 | builder: (_) => const Scaffold( 97 | body: Center( 98 | child: Text('Page Not Found :('), 99 | ), 100 | ), 101 | ); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/presentation/pages/member/order_success_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:avatar_glow/avatar_glow.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | import 'package:fic_mini_project/common/routes.dart'; 7 | import 'package:fic_mini_project/common/styles.dart'; 8 | import 'package:fic_mini_project/presentation/blocs/profile/profile_bloc.dart'; 9 | 10 | class OrderSuccessPage extends StatelessWidget { 11 | const OrderSuccessPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | body: SafeArea( 17 | child: Container( 18 | width: MediaQuery.of(context).size.width, 19 | height: MediaQuery.of(context).size.height, 20 | padding: const EdgeInsets.symmetric(horizontal: 24), 21 | decoration: const BoxDecoration( 22 | gradient: LinearGradient( 23 | colors: [ 24 | Color(0xff1488CC), 25 | Color(0xff2B32B2), 26 | ], 27 | begin: Alignment.bottomCenter, 28 | end: Alignment.topCenter, 29 | ), 30 | ), 31 | child: Column( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | children: [ 34 | Container( 35 | decoration: BoxDecoration( 36 | color: whiteColor, 37 | borderRadius: BorderRadius.circular(10), 38 | ), 39 | padding: const EdgeInsets.all(32), 40 | child: Column( 41 | children: [ 42 | SizedBox( 43 | height: 161, 44 | width: 161, 45 | child: AvatarGlow( 46 | glowColor: Colors.blue, 47 | endRadius: 80, 48 | child: Material( 49 | elevation: 8, 50 | shape: const CircleBorder(), 51 | child: CircleAvatar( 52 | radius: 50, 53 | child: Image.asset('assets/success_order.png'), 54 | ), 55 | ), 56 | ), 57 | ), 58 | const SizedBox(height: 24), 59 | Text( 60 | 'Selamat', 61 | style: 62 | Theme.of(context).textTheme.headlineSmall!.copyWith( 63 | color: blueColor, 64 | fontWeight: FontWeight.w700, 65 | ), 66 | textAlign: TextAlign.center, 67 | ), 68 | const SizedBox(height: 6), 69 | Text( 70 | 'Transaksi Anda Telah Berhasil !', 71 | style: Theme.of(context) 72 | .textTheme 73 | .titleLarge! 74 | .copyWith(color: blueColor), 75 | textAlign: TextAlign.center, 76 | ), 77 | ], 78 | ), 79 | ), 80 | const SizedBox(height: 100), 81 | OutlinedButton( 82 | onPressed: () { 83 | Navigator.pop(context); 84 | context.read().add(OnFetchProfile()); 85 | }, 86 | style: OutlinedButton.styleFrom( 87 | side: const BorderSide( 88 | color: whiteColor, 89 | width: 2, 90 | ), 91 | shape: RoundedRectangleBorder( 92 | borderRadius: BorderRadius.circular(5), 93 | ), 94 | fixedSize: Size(MediaQuery.of(context).size.width, 57), 95 | ), 96 | child: Text( 97 | 'Kembali Ke Beranda', 98 | style: Theme.of(context) 99 | .textTheme 100 | .labelLarge! 101 | .copyWith(color: whiteColor), 102 | ), 103 | ), 104 | const SizedBox(height: 16), 105 | ElevatedButton( 106 | onPressed: () { 107 | Navigator.pushReplacementNamed( 108 | context, 109 | memberTransactionRoute, 110 | ); 111 | context.read().add(OnFetchProfile()); 112 | }, 113 | style: ElevatedButton.styleFrom( 114 | backgroundColor: whiteColor, 115 | shape: RoundedRectangleBorder( 116 | borderRadius: BorderRadius.circular(5), 117 | ), 118 | fixedSize: Size(MediaQuery.of(context).size.width, 57), 119 | ), 120 | child: Text( 121 | 'Lihat Riwayat Transaksi', 122 | style: Theme.of(context) 123 | .textTheme 124 | .labelLarge! 125 | .copyWith(color: blueColor), 126 | ), 127 | ), 128 | ], 129 | ), 130 | ), 131 | ), 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/presentation/pages/vendor/transaction_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import 'package:fic_mini_project/common/currency_rupiah_extension.dart'; 6 | import 'package:fic_mini_project/common/datetime_extension.dart'; 7 | import 'package:fic_mini_project/common/styles.dart'; 8 | import 'package:fic_mini_project/domain/entity/transaction.dart'; 9 | import 'package:fic_mini_project/presentation/blocs/transaction/transaction_bloc.dart'; 10 | 11 | class TransactionPage extends StatefulWidget { 12 | const TransactionPage({super.key}); 13 | 14 | @override 15 | State createState() => _TransactionPageState(); 16 | } 17 | 18 | class _TransactionPageState extends State { 19 | @override 20 | void initState() { 21 | super.initState(); 22 | context.read().add(OnFetchAllTransactions()); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | backgroundColor: greyColor, 29 | appBar: AppBar( 30 | title: const Text('Riwayat Transaksi'), 31 | ), 32 | body: BlocBuilder( 33 | builder: (context, state) { 34 | if (state is TransactionLoading) { 35 | return const Center( 36 | child: CircularProgressIndicator(), 37 | ); 38 | } else if (state is AllTransactionsLoaded) { 39 | return RefreshIndicator( 40 | onRefresh: () async { 41 | context.read().add(OnFetchAllTransactions()); 42 | }, 43 | child: ListView.builder( 44 | padding: const EdgeInsets.only( 45 | left: 16, 46 | right: 16, 47 | top: 16, 48 | ), 49 | physics: const BouncingScrollPhysics(), 50 | itemCount: state.transactions.length, 51 | itemBuilder: (_, index) { 52 | final transaction = state.transactions[index]; 53 | return _TransactionCard(transaction: transaction); 54 | }, 55 | ), 56 | ); 57 | } else if (state is TransactionEmpty) { 58 | return const Center( 59 | child: Text('Data Kosong'), 60 | ); 61 | } else if (state is TransactionFailure) { 62 | return Center( 63 | child: Text(state.message), 64 | ); 65 | } else { 66 | return Container(); 67 | } 68 | }, 69 | ), 70 | ); 71 | } 72 | } 73 | 74 | class _TransactionCard extends StatelessWidget { 75 | const _TransactionCard({ 76 | required this.transaction, 77 | }); 78 | 79 | final TransactionEntity transaction; 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return Container( 84 | margin: const EdgeInsets.only(bottom: 16), 85 | padding: const EdgeInsets.all(16), 86 | decoration: BoxDecoration( 87 | color: whiteColor, 88 | borderRadius: BorderRadius.circular(16), 89 | ), 90 | child: Column( 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | children: [ 93 | Row( 94 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 95 | children: [ 96 | Text( 97 | transaction.createdAt.dateTimeFormatter, 98 | style: Theme.of(context).textTheme.bodyLarge, 99 | ), 100 | Row( 101 | children: [ 102 | const Icon( 103 | Icons.check_circle_outline_rounded, 104 | color: Colors.green, 105 | ), 106 | const SizedBox(width: 4), 107 | Text( 108 | 'Selesai', 109 | style: Theme.of(context) 110 | .textTheme 111 | .bodyMedium! 112 | .copyWith(color: Colors.green), 113 | ), 114 | ], 115 | ), 116 | ], 117 | ), 118 | const Padding( 119 | padding: EdgeInsets.symmetric(vertical: 16), 120 | child: Divider(thickness: 0.2, height: 1), 121 | ), 122 | const Text('Nama Member :'), 123 | const SizedBox(height: 4), 124 | Text( 125 | transaction.user.name!, 126 | style: Theme.of(context) 127 | .textTheme 128 | .bodyLarge! 129 | .copyWith(color: blueColor), 130 | ), 131 | const SizedBox(height: 16), 132 | Row( 133 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 134 | children: [ 135 | Column( 136 | crossAxisAlignment: CrossAxisAlignment.start, 137 | children: [ 138 | const Text('Total :'), 139 | const SizedBox(height: 4), 140 | Text( 141 | transaction.endTotalPrice.intToFormatRupiah, 142 | style: Theme.of(context) 143 | .textTheme 144 | .bodyLarge! 145 | .copyWith(color: blueColor), 146 | ), 147 | ], 148 | ), 149 | Text( 150 | transaction.paymentMethod, 151 | style: Theme.of(context) 152 | .textTheme 153 | .bodyLarge! 154 | .copyWith(color: blueColor), 155 | ), 156 | ], 157 | ), 158 | ], 159 | ), 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lib/presentation/pages/profile/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import 'package:fic_mini_project/common/routes.dart'; 6 | import 'package:fic_mini_project/common/styles.dart'; 7 | import 'package:fic_mini_project/domain/entity/user.dart'; 8 | import 'package:fic_mini_project/presentation/blocs/auth/auth_bloc.dart'; 9 | import 'package:fic_mini_project/presentation/blocs/profile/profile_bloc.dart'; 10 | 11 | class ProfilePage extends StatelessWidget { 12 | const ProfilePage({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: const Text('Profil'), 19 | ), 20 | body: BlocListener( 21 | listener: (context, state) { 22 | if (state is LogoutSuccess) { 23 | Navigator.pushNamedAndRemoveUntil( 24 | context, loginRoute, (_) => false); 25 | } 26 | }, 27 | listenWhen: (previous, current) { 28 | return previous != current && current is LogoutSuccess; 29 | }, 30 | child: BlocBuilder( 31 | builder: (_, state) { 32 | if (state is ProfileLoading) { 33 | return const Center( 34 | child: CircularProgressIndicator(), 35 | ); 36 | } else if (state is ProfileLoaded) { 37 | return _ContentProfile(user: state.user); 38 | } else if (state is ProfileFailure) { 39 | return Center( 40 | child: Text(state.message), 41 | ); 42 | } else { 43 | return Container(); 44 | } 45 | }, 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | 52 | class _ContentProfile extends StatelessWidget { 53 | const _ContentProfile({ 54 | required this.user, 55 | }); 56 | 57 | final User user; 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return Padding( 62 | padding: const EdgeInsets.all(24), 63 | child: Column( 64 | children: [ 65 | Row( 66 | children: [ 67 | user.photoUrl != null 68 | ? CircleAvatar( 69 | radius: 50, 70 | backgroundImage: NetworkImage(user.photoUrl!), 71 | backgroundColor: Colors.grey[300], 72 | ) 73 | : CircleAvatar( 74 | radius: 50, 75 | backgroundColor: Colors.grey[300], 76 | child: const Icon(Icons.person, size: 48), 77 | ), 78 | const SizedBox(width: 14), 79 | Column( 80 | crossAxisAlignment: CrossAxisAlignment.start, 81 | children: [ 82 | Text( 83 | '${user.name}', 84 | style: Theme.of(context).textTheme.headlineSmall!.copyWith( 85 | color: navyColor, 86 | fontWeight: FontWeight.w700, 87 | ), 88 | ), 89 | const SizedBox(height: 4), 90 | Text( 91 | '${user.email}', 92 | style: Theme.of(context) 93 | .textTheme 94 | .titleMedium! 95 | .copyWith(color: navyColor), 96 | ), 97 | ], 98 | ), 99 | ], 100 | ), 101 | const SizedBox(height: 40), 102 | _ListMenuProfile( 103 | onPressed: () { 104 | Navigator.pushNamed(context, updateProfileRoute); 105 | }, 106 | text: 'Ubah Profil', 107 | ), 108 | const SizedBox(height: 8), 109 | _ListMenuProfile( 110 | onPressed: () => showDialog( 111 | context: context, 112 | barrierDismissible: false, 113 | builder: (_) => const _ConfirmLogoutDialog(), 114 | ), 115 | text: 'Logout', 116 | ), 117 | ], 118 | ), 119 | ); 120 | } 121 | } 122 | 123 | class _ListMenuProfile extends StatelessWidget { 124 | const _ListMenuProfile({ 125 | required this.onPressed, 126 | required this.text, 127 | }); 128 | 129 | final Function()? onPressed; 130 | final String text; 131 | 132 | @override 133 | Widget build(BuildContext context) { 134 | return Card( 135 | shape: RoundedRectangleBorder( 136 | borderRadius: BorderRadius.circular(8), 137 | ), 138 | elevation: 4, 139 | child: ListTile( 140 | onTap: onPressed, 141 | title: Text(text), 142 | trailing: const Icon(Icons.arrow_forward_ios), 143 | shape: RoundedRectangleBorder( 144 | borderRadius: BorderRadius.circular(8), 145 | ), 146 | ), 147 | ); 148 | } 149 | } 150 | 151 | class _ConfirmLogoutDialog extends StatelessWidget { 152 | const _ConfirmLogoutDialog(); 153 | 154 | @override 155 | Widget build(BuildContext context) { 156 | return AlertDialog( 157 | shape: RoundedRectangleBorder( 158 | borderRadius: BorderRadius.circular(16), 159 | ), 160 | title: Text( 161 | 'Logout', 162 | style: 163 | Theme.of(context).textTheme.titleLarge!.copyWith(color: navyColor), 164 | ), 165 | icon: const Icon(Icons.logout), 166 | content: const Text( 167 | 'Apakah Anda ingin logout ?', 168 | textAlign: TextAlign.center, 169 | ), 170 | actionsAlignment: MainAxisAlignment.center, 171 | actions: [ 172 | ElevatedButton( 173 | onPressed: () { 174 | context.read().add(LogoutRequested()); 175 | }, 176 | style: ElevatedButton.styleFrom( 177 | shape: RoundedRectangleBorder( 178 | borderRadius: BorderRadius.circular(16), 179 | ), 180 | ), 181 | child: const Text('Ya'), 182 | ), 183 | ElevatedButton( 184 | onPressed: () => Navigator.pop(context), 185 | style: ElevatedButton.styleFrom( 186 | shape: RoundedRectangleBorder( 187 | borderRadius: BorderRadius.circular(16), 188 | ), 189 | ), 190 | child: const Text('Tidak'), 191 | ), 192 | ], 193 | ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/presentation/pages/vendor/product_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import 'package:fic_mini_project/common/currency_rupiah_extension.dart'; 6 | import 'package:fic_mini_project/common/routes.dart'; 7 | import 'package:fic_mini_project/common/styles.dart'; 8 | import 'package:fic_mini_project/domain/entity/product.dart'; 9 | import 'package:fic_mini_project/presentation/blocs/product/product_bloc.dart'; 10 | import 'package:fic_mini_project/presentation/widgets/action_dialog.dart'; 11 | import 'package:fic_mini_project/presentation/widgets/confirm_delete_dialog.dart'; 12 | import 'package:fic_mini_project/presentation/widgets/error_dialog.dart'; 13 | 14 | class ProductPage extends StatefulWidget { 15 | const ProductPage({super.key}); 16 | 17 | @override 18 | State createState() => _ProductPageState(); 19 | } 20 | 21 | class _ProductPageState extends State { 22 | final _scaffoldMessengerKey = GlobalKey(); 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | Future.microtask( 28 | () => context.read().add(OnFetchAllProducts()), 29 | ); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return BlocListener( 35 | listener: (context, state) { 36 | if (state is ProductActionSuccess) { 37 | _scaffoldMessengerKey.currentState! 38 | ..removeCurrentSnackBar() 39 | ..showSnackBar( 40 | SnackBar( 41 | content: Text(state.message), 42 | backgroundColor: Colors.green[400], 43 | ), 44 | ); 45 | } 46 | 47 | if (state is ProductActionFailure) { 48 | showDialog( 49 | context: context, 50 | barrierDismissible: false, 51 | builder: (_) => ErrorDialog(message: state.message), 52 | ); 53 | } 54 | }, 55 | child: ScaffoldMessenger( 56 | key: _scaffoldMessengerKey, 57 | child: Scaffold( 58 | backgroundColor: greyColor, 59 | appBar: AppBar( 60 | title: const Text('Produk'), 61 | ), 62 | body: BlocBuilder( 63 | builder: (_, state) { 64 | if (state is ProductLoading) { 65 | return const Center( 66 | child: CircularProgressIndicator(), 67 | ); 68 | } else if (state is ProductEmpty) { 69 | return const Center( 70 | child: Text('Data Kosong'), 71 | ); 72 | } else if (state is ProductFailure) { 73 | return Center( 74 | child: Text(state.message), 75 | ); 76 | } else if (state is AllProductsLoaded) { 77 | return ListView.builder( 78 | itemCount: state.products.length, 79 | padding: const EdgeInsets.only( 80 | top: 24, 81 | left: 24, 82 | right: 24, 83 | bottom: 4, 84 | ), 85 | itemBuilder: (_, index) { 86 | final product = state.products[index]; 87 | return _ProductCard(product: product); 88 | }, 89 | ); 90 | } else { 91 | return Container(); 92 | } 93 | }, 94 | ), 95 | floatingActionButton: BlocBuilder( 96 | buildWhen: (_, current) => current is! ProductLoading, 97 | builder: (context, _) { 98 | return FloatingActionButton( 99 | onPressed: () => 100 | Navigator.pushNamed(context, productAddUpdateRoute), 101 | child: const Icon(Icons.add), 102 | ); 103 | }, 104 | ), 105 | ), 106 | ), 107 | ); 108 | } 109 | } 110 | 111 | class _ProductCard extends StatelessWidget { 112 | const _ProductCard({ 113 | required this.product, 114 | }); 115 | 116 | final Product product; 117 | 118 | @override 119 | Widget build(BuildContext context) { 120 | return Card( 121 | elevation: 0, 122 | shape: RoundedRectangleBorder( 123 | borderRadius: BorderRadius.circular(20), 124 | ), 125 | margin: const EdgeInsets.only(bottom: 20), 126 | child: InkWell( 127 | onTap: () { 128 | showDialog( 129 | context: context, 130 | builder: (context) => ActionDialog( 131 | titleUpdateAction: 'Ubah Produk', 132 | updateActionOnTap: () { 133 | Navigator.pop(context); 134 | Navigator.pushNamed( 135 | context, 136 | productAddUpdateRoute, 137 | arguments: product, 138 | ); 139 | }, 140 | titleDeleteAction: 'Hapus Produk', 141 | deleteActionOnTap: () { 142 | Navigator.pop(context); 143 | showDialog( 144 | context: context, 145 | builder: (context) { 146 | return ConfirmDeleteDialog( 147 | title: 'Hapus Produk', 148 | yesOnPressed: () { 149 | Navigator.pop(context); 150 | context 151 | .read() 152 | .add(OnDeleteProduct(product)); 153 | }, 154 | ); 155 | }, 156 | ); 157 | }, 158 | ), 159 | ); 160 | }, 161 | borderRadius: BorderRadius.circular(20), 162 | child: Row( 163 | children: [ 164 | ClipRRect( 165 | borderRadius: BorderRadius.circular(20), 166 | child: Image.memory( 167 | product.image!, 168 | width: 120, 169 | height: 100, 170 | fit: BoxFit.cover, 171 | ), 172 | ), 173 | Expanded( 174 | child: Padding( 175 | padding: const EdgeInsets.symmetric( 176 | vertical: 14, 177 | horizontal: 15, 178 | ), 179 | child: Column( 180 | crossAxisAlignment: CrossAxisAlignment.start, 181 | children: [ 182 | Text( 183 | product.name, 184 | style: Theme.of(context).textTheme.titleSmall, 185 | ), 186 | const SizedBox(height: 4), 187 | Text( 188 | product.category!.name, 189 | style: Theme.of(context).textTheme.bodySmall, 190 | ), 191 | const SizedBox(height: 20), 192 | Text( 193 | product.price.intToFormatRupiah, 194 | style: Theme.of(context).textTheme.titleMedium!.copyWith( 195 | color: blueColor, 196 | fontWeight: FontWeight.w700, 197 | ), 198 | ), 199 | ], 200 | ), 201 | ), 202 | ), 203 | ], 204 | ), 205 | ), 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/presentation/pages/vendor/pos_checkout_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:qr_flutter/qr_flutter.dart'; 6 | 7 | import 'package:fic_mini_project/common/currency_rupiah_extension.dart'; 8 | import 'package:fic_mini_project/common/routes.dart'; 9 | import 'package:fic_mini_project/common/styles.dart'; 10 | import 'package:fic_mini_project/domain/entity/cart.dart'; 11 | 12 | class PosCheckoutPage extends StatelessWidget { 13 | const PosCheckoutPage({ 14 | required this.cartMap, 15 | required this.cart, 16 | super.key, 17 | }); 18 | 19 | final Map cartMap; 20 | final Cart cart; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Scaffold( 25 | backgroundColor: greyColor, 26 | appBar: AppBar( 27 | title: const Text('POS Checkout'), 28 | automaticallyImplyLeading: false, 29 | ), 30 | body: Column( 31 | children: [ 32 | Expanded( 33 | child: SingleChildScrollView( 34 | child: Container( 35 | padding: const EdgeInsets.all(24), 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.center, 38 | children: [ 39 | Container( 40 | padding: const EdgeInsets.all(24), 41 | decoration: BoxDecoration( 42 | color: whiteColor, 43 | borderRadius: BorderRadius.circular(20), 44 | ), 45 | child: Column( 46 | crossAxisAlignment: CrossAxisAlignment.center, 47 | children: [ 48 | Text( 49 | 'Tunjukan QR Code Kepada Pembeli', 50 | style: Theme.of(context).textTheme.titleLarge, 51 | ), 52 | const SizedBox(height: 16), 53 | QrImageView( 54 | data: jsonEncode(cartMap), 55 | size: 280, 56 | ), 57 | const SizedBox(height: 16), 58 | Text( 59 | 'Tunggu konfirmasi pembeli \nsebelum tutup halaman ini', 60 | style: Theme.of(context).textTheme.titleMedium, 61 | textAlign: TextAlign.center, 62 | ), 63 | ], 64 | ), 65 | ), 66 | const SizedBox(height: 24), 67 | Card( 68 | clipBehavior: Clip.antiAlias, 69 | shape: RoundedRectangleBorder( 70 | borderRadius: BorderRadius.circular(20), 71 | ), 72 | child: ExpansionTile( 73 | controlAffinity: ListTileControlAffinity.leading, 74 | tilePadding: const EdgeInsets.symmetric( 75 | vertical: 8, 76 | horizontal: 16, 77 | ), 78 | title: Text( 79 | 'Detail Transaksi', 80 | style: 81 | Theme.of(context).textTheme.titleMedium!.copyWith( 82 | fontWeight: FontWeight.w700, 83 | color: navyColor, 84 | ), 85 | ), 86 | childrenPadding: const EdgeInsets.only( 87 | left: 16, 88 | right: 16, 89 | bottom: 16, 90 | ), 91 | children: [ 92 | ListView.builder( 93 | shrinkWrap: true, 94 | physics: const NeverScrollableScrollPhysics(), 95 | padding: const EdgeInsets.symmetric(vertical: 4), 96 | itemBuilder: (context, index) { 97 | final product = cart.products[index]; 98 | return Container( 99 | margin: const EdgeInsets.symmetric(vertical: 4), 100 | child: ListTile( 101 | tileColor: greyColor, 102 | shape: RoundedRectangleBorder( 103 | borderRadius: BorderRadius.circular(8), 104 | ), 105 | title: Text(product.name), 106 | subtitle: Text( 107 | product.price.intToFormatRupiah, 108 | style: Theme.of(context) 109 | .textTheme 110 | .bodyMedium! 111 | .copyWith( 112 | color: blueColor, 113 | ), 114 | ), 115 | trailing: Row( 116 | mainAxisSize: MainAxisSize.min, 117 | children: [ 118 | Text( 119 | product.quantity.toString(), 120 | style: Theme.of(context) 121 | .textTheme 122 | .bodyMedium! 123 | .copyWith( 124 | fontWeight: FontWeight.w700), 125 | ), 126 | const SizedBox(width: 4), 127 | const Text('item'), 128 | ], 129 | ), 130 | ), 131 | ); 132 | }, 133 | itemCount: cart.products.length, 134 | ), 135 | const SizedBox(height: 8), 136 | ListTile( 137 | tileColor: Colors.grey[300], 138 | shape: RoundedRectangleBorder( 139 | borderRadius: BorderRadius.circular(8), 140 | ), 141 | title: const Text('Total'), 142 | trailing: Text( 143 | cart.totalPrice.intToFormatRupiah, 144 | style: Theme.of(context) 145 | .textTheme 146 | .bodyMedium! 147 | .copyWith(fontWeight: FontWeight.w700), 148 | ), 149 | ), 150 | ], 151 | ), 152 | ), 153 | ], 154 | ), 155 | ), 156 | ), 157 | ), 158 | Container( 159 | padding: const EdgeInsets.all(24), 160 | child: Column( 161 | children: [ 162 | OutlinedButton( 163 | onPressed: () => Navigator.pop(context), 164 | style: OutlinedButton.styleFrom( 165 | side: const BorderSide( 166 | width: 2, 167 | color: blueColor, 168 | ), 169 | shape: RoundedRectangleBorder( 170 | borderRadius: BorderRadius.circular(5), 171 | ), 172 | fixedSize: Size(MediaQuery.of(context).size.width, 57), 173 | ), 174 | child: const Text('Kembali Ke Beranda'), 175 | ), 176 | const SizedBox(height: 16), 177 | ElevatedButton( 178 | onPressed: () => 179 | Navigator.pushReplacementNamed(context, transactionRoute), 180 | style: ElevatedButton.styleFrom( 181 | backgroundColor: Colors.green, 182 | shape: RoundedRectangleBorder( 183 | borderRadius: BorderRadius.circular(5), 184 | ), 185 | fixedSize: Size(MediaQuery.of(context).size.width, 57), 186 | ), 187 | child: const Text('Cek Status Transaksi'), 188 | ), 189 | ], 190 | ), 191 | ), 192 | ], 193 | ), 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/presentation/pages/profile/update_profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:image_picker/image_picker.dart'; 7 | 8 | import 'package:fic_mini_project/common/styles.dart'; 9 | import 'package:fic_mini_project/domain/entity/user.dart'; 10 | import 'package:fic_mini_project/presentation/blocs/profile/profile_bloc.dart'; 11 | import 'package:fic_mini_project/presentation/widgets/text_form_label.dart'; 12 | 13 | class UpdateProfilePage extends StatefulWidget { 14 | const UpdateProfilePage({super.key}); 15 | 16 | @override 17 | State createState() => _UpdateProfilePageState(); 18 | } 19 | 20 | class _UpdateProfilePageState extends State { 21 | final _scaffoldMessengerKey = GlobalKey(); 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | Future.microtask(() => context.read().add(OnFetchProfile())); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return ScaffoldMessenger( 32 | key: _scaffoldMessengerKey, 33 | child: Scaffold( 34 | appBar: AppBar( 35 | title: const Text('Ubah Profil'), 36 | ), 37 | body: BlocListener( 38 | listener: (context, state) { 39 | if (state is ProfileUpdateFailure) { 40 | _scaffoldMessengerKey.currentState! 41 | ..removeCurrentSnackBar() 42 | ..showSnackBar( 43 | SnackBar( 44 | backgroundColor: Colors.red[400], 45 | content: Text(state.message), 46 | ), 47 | ); 48 | } 49 | 50 | if (state is ProfileUpdateSuccess) { 51 | _scaffoldMessengerKey.currentState! 52 | ..removeCurrentSnackBar() 53 | ..showSnackBar( 54 | SnackBar( 55 | backgroundColor: Colors.green[400], 56 | content: Text(state.message), 57 | ), 58 | ); 59 | } 60 | }, 61 | child: BlocBuilder( 62 | builder: (_, state) { 63 | if (state is ProfileLoading) { 64 | return const Center( 65 | child: CircularProgressIndicator(), 66 | ); 67 | } else if (state is ProfileLoaded) { 68 | return _ContentUpdateProfile(user: state.user); 69 | } else if (state is ProfileFailure) { 70 | return Center( 71 | child: Text(state.message), 72 | ); 73 | } else { 74 | return Container(); 75 | } 76 | }, 77 | ), 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | 84 | class _ContentUpdateProfile extends StatefulWidget { 85 | const _ContentUpdateProfile({ 86 | required this.user, 87 | }); 88 | 89 | final User user; 90 | 91 | @override 92 | State<_ContentUpdateProfile> createState() => _ContentUpdateProfileState(); 93 | } 94 | 95 | class _ContentUpdateProfileState extends State<_ContentUpdateProfile> { 96 | final _formKey = GlobalKey(); 97 | 98 | final _nameController = TextEditingController(); 99 | final _phoneNumberController = TextEditingController(); 100 | 101 | XFile? _imageProfile; 102 | 103 | @override 104 | void initState() { 105 | super.initState(); 106 | 107 | _nameController.text = widget.user.name ?? ''; 108 | _phoneNumberController.text = widget.user.phoneNumber ?? ''; 109 | } 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | return SingleChildScrollView( 114 | child: Padding( 115 | padding: const EdgeInsets.all(24), 116 | child: Form( 117 | key: _formKey, 118 | autovalidateMode: AutovalidateMode.onUserInteraction, 119 | child: Column( 120 | crossAxisAlignment: CrossAxisAlignment.start, 121 | children: [ 122 | Center( 123 | child: InkWell( 124 | borderRadius: BorderRadius.circular(16), 125 | onTap: () async { 126 | final imagePicker = ImagePicker(); 127 | final image = await imagePicker.pickImage( 128 | source: ImageSource.gallery); 129 | if (image != null) { 130 | setState(() { 131 | _imageProfile = image; 132 | }); 133 | } 134 | }, 135 | child: SizedBox( 136 | height: 100, 137 | width: 100, 138 | child: Stack( 139 | children: [ 140 | _imageProfile != null 141 | ? CircleAvatar( 142 | radius: 50, 143 | backgroundColor: Colors.grey[300], 144 | backgroundImage: FileImage( 145 | File(_imageProfile!.path), 146 | ), 147 | ) 148 | : widget.user.photoUrl != null 149 | ? CircleAvatar( 150 | radius: 50, 151 | backgroundColor: Colors.grey[300], 152 | backgroundImage: 153 | NetworkImage(widget.user.photoUrl!), 154 | ) 155 | : CircleAvatar( 156 | radius: 50, 157 | backgroundColor: Colors.grey[300], 158 | child: const Icon(Icons.person, size: 48), 159 | ), 160 | Align( 161 | alignment: Alignment.bottomRight, 162 | child: Container( 163 | padding: const EdgeInsets.all(4), 164 | decoration: BoxDecoration( 165 | color: blueColor, 166 | borderRadius: BorderRadius.circular(8), 167 | ), 168 | child: const Icon( 169 | Icons.edit, 170 | color: whiteColor, 171 | size: 18, 172 | ), 173 | ), 174 | ), 175 | ], 176 | ), 177 | ), 178 | ), 179 | ), 180 | const SizedBox(height: 30), 181 | const TextFormLabel(label: 'Nama'), 182 | const SizedBox(height: 10), 183 | TextFormField( 184 | controller: _nameController, 185 | decoration: const InputDecoration( 186 | hintText: 'Masukkan Nama', 187 | ), 188 | keyboardType: TextInputType.name, 189 | textCapitalization: TextCapitalization.words, 190 | validator: (value) { 191 | if (value.toString().isEmpty) { 192 | return 'Nama tidak boleh kosong'; 193 | } 194 | return null; 195 | }, 196 | ), 197 | const SizedBox(height: 20), 198 | const TextFormLabel(label: 'No. Handphone'), 199 | const SizedBox(height: 10), 200 | TextFormField( 201 | controller: _phoneNumberController, 202 | decoration: const InputDecoration( 203 | hintText: 'Masukkan No. Handphone', 204 | ), 205 | keyboardType: TextInputType.phone, 206 | validator: (value) { 207 | if (value.toString().isEmpty) { 208 | return 'No. Handphone tidak boleh kosong'; 209 | } 210 | return null; 211 | }, 212 | ), 213 | const SizedBox(height: 50), 214 | ElevatedButton.icon( 215 | onPressed: () { 216 | if (_formKey.currentState!.validate()) { 217 | final dataInput = User( 218 | id: widget.user.id, 219 | name: _nameController.text, 220 | email: widget.user.email, 221 | phoneNumber: _phoneNumberController.text, 222 | photoUrl: widget.user.photoUrl, 223 | point: widget.user.point, 224 | ); 225 | 226 | context 227 | .read() 228 | .add(OnUpdateProfile(dataInput, _imageProfile)); 229 | } 230 | }, 231 | icon: const Icon(Icons.save), 232 | label: const Text('Simpan'), 233 | style: ElevatedButton.styleFrom( 234 | fixedSize: Size(MediaQuery.of(context).size.width, 57), 235 | ), 236 | ), 237 | ], 238 | ), 239 | ), 240 | ), 241 | ); 242 | } 243 | } 244 | --------------------------------------------------------------------------------