├── assets ├── images │ ├── avatar.png │ ├── 2x │ │ ├── avatar.png │ │ └── imgLogo.png │ ├── 3x │ │ ├── avatar.png │ │ └── imgLogo.png │ ├── imgLogo.png │ └── background_image_chair.jpg └── fonts │ ├── barbershop_icons.ttf │ └── Poppins │ ├── Poppins-Bold.ttf │ ├── Poppins-Thin.ttf │ ├── Poppins-Black.ttf │ ├── Poppins-Italic.ttf │ ├── Poppins-Light.ttf │ ├── Poppins-Medium.ttf │ ├── Poppins-ExtraBold.ttf │ ├── Poppins-Regular.ttf │ ├── Poppins-SemiBold.ttf │ ├── Poppins-BlackItalic.ttf │ ├── Poppins-BoldItalic.ttf │ ├── Poppins-ExtraLight.ttf │ ├── Poppins-LightItalic.ttf │ ├── Poppins-ThinItalic.ttf │ ├── Poppins-MediumItalic.ttf │ ├── Poppins-SemiBoldItalic.ttf │ ├── Poppins-ExtraBoldItalic.ttf │ ├── Poppins-ExtraLightItalic.ttf │ └── OFL.txt ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── felipecastrosales │ │ │ │ │ └── barbershop │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── lib ├── src │ ├── core │ │ ├── constants │ │ │ ├── app_fonts.dart │ │ │ ├── local_storage_keys.dart │ │ │ ├── constants.dart │ │ │ ├── app_images.dart │ │ │ └── app_colors.dart │ │ ├── fp │ │ │ ├── nil.dart │ │ │ └── either.dart │ │ ├── exceptions │ │ │ ├── service_exception.dart │ │ │ ├── repository_exception.dart │ │ │ └── auth_exception.dart │ │ ├── ui │ │ │ ├── helpers │ │ │ │ ├── form_helper.dart │ │ │ │ └── messages.dart │ │ │ ├── barbershop_nav_global_key.dart │ │ │ ├── widgets │ │ │ │ ├── barbershop_loader.dart │ │ │ │ ├── avatar_widget.dart │ │ │ │ ├── weekdays_panel.dart │ │ │ │ └── hours_panel.dart │ │ │ ├── barbershop_icons.dart │ │ │ └── barbershop_theme.dart │ │ ├── rest_client │ │ │ ├── rest_client.dart │ │ │ └── interceptors │ │ │ │ └── auth_interceptor.dart │ │ └── providers │ │ │ ├── application_providers.dart │ │ │ └── application_providers.g.dart │ ├── services │ │ ├── user_login │ │ │ ├── user_login_service.dart │ │ │ └── user_login_service_impl.dart │ │ └── user_register_adm │ │ │ ├── user_service_adm.dart │ │ │ └── user_service_adm_impl.dart │ ├── features │ │ ├── home │ │ │ ├── adm │ │ │ │ ├── home_adm_state.dart │ │ │ │ ├── home_adm_vm.g.dart │ │ │ │ ├── home_adm_vm.dart │ │ │ │ ├── home_adm_page.dart │ │ │ │ └── widgets │ │ │ │ │ └── home_employee_tile.dart │ │ │ ├── employee │ │ │ │ ├── home_employee_provider.dart │ │ │ │ ├── home_employee_provider.g.dart │ │ │ │ └── home_employee_page.dart │ │ │ └── widgets │ │ │ │ └── home_header.dart │ │ ├── auth │ │ │ ├── register │ │ │ │ ├── user │ │ │ │ │ ├── user_register_providers.dart │ │ │ │ │ ├── user_register_vm.g.dart │ │ │ │ │ ├── user_register_providers.g.dart │ │ │ │ │ ├── user_register_vm.dart │ │ │ │ │ └── user_register_page.dart │ │ │ │ └── barbershop │ │ │ │ │ ├── barbershop_register_status.dart │ │ │ │ │ ├── barbershop_register_vm.g.dart │ │ │ │ │ ├── barbershop_register_vm.dart │ │ │ │ │ └── barbershop_register_page.dart │ │ │ └── login │ │ │ │ ├── login_state.dart │ │ │ │ ├── login_vm.g.dart │ │ │ │ ├── login_vm.dart │ │ │ │ └── login_page.dart │ │ ├── schedule │ │ │ ├── schedule_state.dart │ │ │ ├── schedule_vm.g.dart │ │ │ ├── schedule_vm.dart │ │ │ ├── widgets │ │ │ │ └── schedule_calendar.dart │ │ │ └── schedule_page.dart │ │ ├── splash │ │ │ ├── splash_vm.g.dart │ │ │ ├── splash_vm.dart │ │ │ └── splash_page.dart │ │ └── employee │ │ │ ├── schedule │ │ │ ├── appointment_data_source.dart │ │ │ ├── employee_schedule_vm.dart │ │ │ ├── employee_schedule_vm.g.dart │ │ │ └── employee_schedule_page.dart │ │ │ └── register │ │ │ ├── employee_register_vm.g.dart │ │ │ ├── employee_register_state.dart │ │ │ ├── employee_register_vm.dart │ │ │ └── employee_register_page.dart │ ├── repositories │ │ ├── schedule │ │ │ ├── schedule_repository.dart │ │ │ └── schedule_repository_impl.dart │ │ ├── barbershop │ │ │ ├── barbershop_repository.dart │ │ │ └── barbershop_repository_impl.dart │ │ └── user │ │ │ ├── user_repository.dart │ │ │ └── user_repository_impl.dart │ ├── models │ │ ├── barbershop_model.dart │ │ ├── schedule_model.dart │ │ └── user_model.dart │ └── barbershop_app.dart └── main.dart ├── api ├── config.yaml ├── postman │ ├── Employee.postman_collection.json │ ├── Auth.postman_collection.json │ └── Barbershop.postman_collection.json └── database.json ├── analysis_options.yaml ├── .vscode └── launch.json ├── .gitignore ├── .metadata ├── LICENSE ├── pubspec.yaml └── README.md /assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/avatar.png -------------------------------------------------------------------------------- /assets/images/2x/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/2x/avatar.png -------------------------------------------------------------------------------- /assets/images/3x/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/3x/avatar.png -------------------------------------------------------------------------------- /assets/images/imgLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/imgLogo.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/images/2x/imgLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/2x/imgLogo.png -------------------------------------------------------------------------------- /assets/images/3x/imgLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/3x/imgLogo.png -------------------------------------------------------------------------------- /lib/src/core/constants/app_fonts.dart: -------------------------------------------------------------------------------- 1 | sealed class AppFonts { 2 | static const String fontFamily = 'Poppins'; 3 | } 4 | -------------------------------------------------------------------------------- /assets/fonts/barbershop_icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/barbershop_icons.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Thin.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Black.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Medium.ttf -------------------------------------------------------------------------------- /lib/src/core/constants/local_storage_keys.dart: -------------------------------------------------------------------------------- 1 | sealed class LocalStorageKeys { 2 | static const accessToken = 'ACCESS_TOKEN_KEY'; 3 | } 4 | -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /assets/images/background_image_chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/images/background_image_chair.jpg -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-BlackItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-ExtraLight.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-LightItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-ThinItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-MediumItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /lib/src/core/fp/nil.dart: -------------------------------------------------------------------------------- 1 | final class Nil { 2 | const Nil(); 3 | 4 | @override 5 | String toString() => 'Nil'; 6 | } 7 | 8 | const nil = Nil(); 9 | -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/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/felipecastrosales/barbershop/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/src/core/constants/constants.dart: -------------------------------------------------------------------------------- 1 | export 'app_colors.dart'; 2 | export 'app_fonts.dart'; 3 | export 'app_images.dart'; 4 | export 'local_storage_keys.dart'; 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/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/felipecastrosales/barbershop/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipecastrosales/barbershop/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/src/core/exceptions/service_exception.dart: -------------------------------------------------------------------------------- 1 | final class ServiceException implements Exception { 2 | const ServiceException({required this.message}); 3 | final String message; 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/core/ui/helpers/form_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension FormExtension on BuildContext { 4 | void unfocus() => FocusScope.of(this).unfocus(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/core/exceptions/repository_exception.dart: -------------------------------------------------------------------------------- 1 | final class RepositoryException implements Exception { 2 | const RepositoryException({required this.message}); 3 | final String message; 4 | } 5 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/felipecastrosales/barbershop/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.felipecastrosales.barbershop 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /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/src/core/constants/app_images.dart: -------------------------------------------------------------------------------- 1 | sealed class AppImages { 2 | static const backgroundChair = 'assets/images/background_image_chair.jpg'; 3 | static const imgLogo = 'assets/images/imgLogo.png'; 4 | static const avatar = 'assets/images/avatar.png'; 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/core/constants/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | sealed class AppColors { 4 | static const brown = Color(0xFFB07B01); 5 | static const grey = Color(0xFF999999); 6 | static const lightGrey = Color(0xFFE6E2E9); 7 | static const red = Color(0xFFEB1212); 8 | } 9 | -------------------------------------------------------------------------------- /api/config.yaml: -------------------------------------------------------------------------------- 1 | name: Json Rest Server 2 | port: 8080 3 | host: 0.0.0.0 4 | database: database.json 5 | 6 | auth: 7 | jwtSecret: cwsMXDtuP447WZQ63nM4dWZ3RppyMl 8 | jwtExpire: 3600 9 | unauthorizedStatusCode: 403 10 | urlSkip: 11 | - /images/: 12 | method: get 13 | - /users: 14 | method: post 15 | -------------------------------------------------------------------------------- /lib/src/core/fp/either.dart: -------------------------------------------------------------------------------- 1 | sealed class Either {} 2 | 3 | class Failure extends Either { 4 | Failure(this.exception); 5 | final E exception; 6 | } 7 | 8 | class Success extends Either { 9 | Success(this.value); 10 | final S value; 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/src/core/ui/barbershop_nav_global_key.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | final class BarbershopNavGlobalKey { 4 | BarbershopNavGlobalKey._(); 5 | 6 | final navKey = GlobalKey(); 7 | 8 | static BarbershopNavGlobalKey? _instance; 9 | static BarbershopNavGlobalKey get instance => 10 | _instance ??= BarbershopNavGlobalKey._(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/barbershop_app.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:intl/date_symbol_data_local.dart'; 5 | 6 | void main() async { 7 | await initializeDateFormatting(); 8 | runApp( 9 | const ProviderScope( 10 | child: BarbershopApp(), 11 | ), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/services/user_login/user_login_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/service_exception.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/fp/nil.dart'; 4 | 5 | abstract interface class UserLoginService { 6 | Future> execute({ 7 | required String email, 8 | required String password, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/core/exceptions/auth_exception.dart: -------------------------------------------------------------------------------- 1 | sealed class AuthException implements Exception { 2 | const AuthException({required this.message}); 3 | final String message; 4 | } 5 | 6 | final class AuthError extends AuthException { 7 | const AuthError({required super.message}); 8 | } 9 | 10 | final class AuthUnauthorizedException extends AuthException { 11 | const AuthUnauthorizedException({super.message = ''}); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/services/user_register_adm/user_service_adm.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/service_exception.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/fp/nil.dart'; 4 | 5 | abstract interface class UserRegisterServiceADM { 6 | Future> execute( 7 | ({String name, String email, String password}) userData, 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/core/ui/widgets/barbershop_loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:loading_animation_widget/loading_animation_widget.dart'; 3 | 4 | class BarbershopLoader extends StatelessWidget { 5 | const BarbershopLoader({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Center( 10 | child: LoadingAnimationWidget.threeArchedCircle( 11 | color: Colors.brown, 12 | size: 60, 13 | ), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/src/features/home/adm/home_adm_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/models/user_model.dart'; 2 | 3 | enum HomeADMStateStatus { loaded, error } 4 | 5 | class HomeADMState { 6 | HomeADMState({ 7 | required this.status, 8 | required this.employees, 9 | }); 10 | 11 | final HomeADMStateStatus status; 12 | final List employees; 13 | 14 | HomeADMState copyWith({ 15 | HomeADMStateStatus? status, 16 | List? employees, 17 | }) => 18 | HomeADMState( 19 | status: status ?? this.status, 20 | employees: employees ?? this.employees, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/core/ui/barbershop_icons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BarbershopIcons { 4 | static const String _fontFamily = 'barbershop_icons'; 5 | 6 | static const IconData addEmployee = IconData(0xe900, fontFamily: _fontFamily); 7 | static const changeAvatar = IconData(0xe901, fontFamily: _fontFamily); 8 | static const exit = IconData(0xe902, fontFamily: _fontFamily); 9 | static const calendar = IconData(0xe909, fontFamily: _fontFamily); 10 | static const search = IconData(0xe943, fontFamily: _fontFamily); 11 | static const penEdit = IconData(0xe944, fontFamily: _fontFamily); 12 | static const trash = IconData(0xe945, fontFamily: _fontFamily); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/user/user_register_providers.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/providers/application_providers.dart'; 2 | import 'package:barbershop/src/services/user_register_adm/user_service_adm.dart'; 3 | import 'package:barbershop/src/services/user_register_adm/user_service_adm_impl.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'user_register_providers.g.dart'; 7 | 8 | @riverpod 9 | UserRegisterServiceADM userRegisterServiceADM(UserRegisterServiceADMRef ref) => 10 | UserRegisterServiceADMImpl( 11 | userRepository: ref.watch(userRepositoryProvider), 12 | userService: ref.watch(userLoginServiceProvider), 13 | ); 14 | -------------------------------------------------------------------------------- /lib/src/core/ui/helpers/messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:top_snackbar_flutter/custom_snack_bar.dart'; 3 | import 'package:top_snackbar_flutter/top_snack_bar.dart'; 4 | 5 | extension Messages on BuildContext { 6 | void showError(String message) => 7 | _showCommonSnackBar(CustomSnackBar.error(message: message)); 8 | 9 | void showSuccess(String message) => 10 | _showCommonSnackBar(CustomSnackBar.success(message: message)); 11 | 12 | void showInfo(String message) => 13 | _showCommonSnackBar(CustomSnackBar.info(message: message)); 14 | 15 | void _showCommonSnackBar(Widget child) => 16 | showTopSnackBar(Overlay.of(this), child); 17 | } 18 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | - always_declare_return_types 6 | - always_use_package_imports 7 | - avoid_dynamic_calls 8 | - avoid_relative_lib_imports 9 | - directives_ordering 10 | - implementation_imports 11 | - require_trailing_commas 12 | - sized_box_for_whitespace 13 | - sized_box_shrink_expand 14 | - sort_child_properties_last 15 | - sort_constructors_first 16 | - prefer_const_constructors 17 | - prefer_const_constructors_in_immutables 18 | - prefer_const_literals_to_create_immutables 19 | - prefer_const_declarations 20 | - prefer_final_fields 21 | - prefer_single_quotes -------------------------------------------------------------------------------- /lib/src/repositories/schedule/schedule_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/fp/nil.dart'; 4 | import 'package:barbershop/src/models/schedule_model.dart'; 5 | 6 | abstract interface class ScheduleRepository { 7 | Future> scheduleClient( 8 | ({ 9 | int barbershopId, 10 | int userId, 11 | String clientName, 12 | DateTime date, 13 | int time, 14 | }) scheduleData, 15 | ); 16 | 17 | Future>> findScheduleByDate( 18 | ({DateTime date, int userId}) filter, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /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.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/repositories/barbershop/barbershop_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/fp/nil.dart'; 4 | import 'package:barbershop/src/models/barbershop_model.dart'; 5 | import 'package:barbershop/src/models/user_model.dart'; 6 | 7 | abstract interface class BarbershopRepository { 8 | Future> getMyBarbershop( 9 | UserModel userModel, 10 | ); 11 | 12 | Future> save( 13 | ({ 14 | String name, 15 | String email, 16 | List openingDays, 17 | List openingHours, 18 | }) data, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/features/auth/login/login_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum LoginStateStatus { 4 | initial, 5 | error, 6 | admLogin, 7 | employeeLogin, 8 | } 9 | 10 | class LoginState { 11 | const LoginState({ 12 | required this.status, 13 | this.errorMessage, 14 | }); 15 | 16 | const LoginState.initial() : this(status: LoginStateStatus.initial); 17 | 18 | final LoginStateStatus status; 19 | final String? errorMessage; 20 | 21 | LoginState copyWith({ 22 | LoginStateStatus? status, 23 | ValueGetter? errorMessage, 24 | }) { 25 | return LoginState( 26 | status: status ?? this.status, 27 | errorMessage: errorMessage != null ? errorMessage() : this.errorMessage, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "barbershop", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "barbershop (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "barbershop (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/core/rest_client/rest_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/rest_client/interceptors/auth_interceptor.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:dio/io.dart'; 4 | import 'package:pretty_dio_logger/pretty_dio_logger.dart'; 5 | 6 | final class RestClient extends DioForNative { 7 | RestClient() 8 | : super( 9 | BaseOptions( 10 | connectTimeout: const Duration(seconds: 10), 11 | receiveTimeout: const Duration(seconds: 60), 12 | baseUrl: 'http://192.168.1.14:8080', 13 | ), 14 | ) { 15 | interceptors.addAll([ 16 | AuthInterceptor(), 17 | PrettyDioLogger(), 18 | ]); 19 | } 20 | 21 | RestClient get auth => this..options.extra['DIO_AUTH_KEY'] = true; 22 | RestClient get unAuth => this..options.extra['DIO_AUTH_KEY'] = false; 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/features/home/employee/home_employee_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/fp/either.dart'; 2 | import 'package:barbershop/src/core/providers/application_providers.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | part 'home_employee_provider.g.dart'; 6 | 7 | @riverpod 8 | Future getTotalSchedulesToday( 9 | GetTotalSchedulesTodayRef ref, 10 | int userId, 11 | ) async { 12 | final DateTime(:year, :month, :day) = DateTime.now(); 13 | final filter = (date: DateTime(year, month, day, 0, 0, 0), userId: userId); 14 | 15 | final scheduleResult = 16 | await ref.read(scheduleRepositoryProvider).findScheduleByDate(filter); 17 | 18 | return switch (scheduleResult) { 19 | Success(value: List(length: final totalSchedules)) => totalSchedules, 20 | Failure(:final exception) => throw exception, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /lib/src/features/schedule/schedule_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum ScheduleStateStatus { 4 | initial, 5 | success, 6 | error, 7 | } 8 | 9 | class ScheduleState { 10 | ScheduleState({ 11 | required this.status, 12 | this.scheduleTime, 13 | this.scheduleDate, 14 | }); 15 | 16 | ScheduleState.initial() : this(status: ScheduleStateStatus.initial); 17 | 18 | final ScheduleStateStatus status; 19 | final int? scheduleTime; 20 | final DateTime? scheduleDate; 21 | 22 | ScheduleState copyWith({ 23 | ScheduleStateStatus? status, 24 | ValueGetter? scheduleHour, 25 | ValueGetter? scheduleDate, 26 | }) => 27 | ScheduleState( 28 | status: status ?? this.status, 29 | scheduleTime: scheduleHour != null ? scheduleHour() : scheduleTime, 30 | scheduleDate: scheduleDate != null ? scheduleDate() : this.scheduleDate, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/features/auth/login/login_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'login_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$loginVMHash() => r'2e103372a528f52d2e11d9b26d5e4241a153a7ba'; 10 | 11 | /// See also [LoginVM]. 12 | @ProviderFor(LoginVM) 13 | final loginVMProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | LoginVM.new, 16 | name: r'loginVMProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$loginVMHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$LoginVM = AutoDisposeNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 26 | -------------------------------------------------------------------------------- /lib/src/features/splash/splash_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'splash_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$splashVMHash() => r'6931c5d24c20266edcd793e0975ca247895c710d'; 10 | 11 | /// See also [SplashVM]. 12 | @ProviderFor(SplashVM) 13 | final splashVMProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | SplashVM.new, 16 | name: r'splashVMProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$splashVMHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$SplashVM = AutoDisposeAsyncNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 26 | -------------------------------------------------------------------------------- /lib/src/features/home/adm/home_adm_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_adm_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$homeADMVMHash() => r'19872c31fb8946de05cfe416e4300e854b8f4c4c'; 10 | 11 | /// See also [HomeADMVM]. 12 | @ProviderFor(HomeADMVM) 13 | final homeADMVMProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | HomeADMVM.new, 16 | name: r'homeADMVMProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$homeADMVMHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$HomeADMVM = AutoDisposeAsyncNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 26 | -------------------------------------------------------------------------------- /lib/src/features/schedule/schedule_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'schedule_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$scheduleVMHash() => r'ef691bfe82728ed7c8de1089c00badd2e6a84092'; 10 | 11 | /// See also [ScheduleVM]. 12 | @ProviderFor(ScheduleVM) 13 | final scheduleVMProvider = 14 | AutoDisposeNotifierProvider.internal( 15 | ScheduleVM.new, 16 | name: r'scheduleVMProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$scheduleVMHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$ScheduleVM = AutoDisposeNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 26 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/barbershop/barbershop_register_status.dart: -------------------------------------------------------------------------------- 1 | enum BarbershopRegisterStateStatus { 2 | initial, 3 | success, 4 | error, 5 | } 6 | 7 | final class BarbershopRegisterState { 8 | const BarbershopRegisterState({ 9 | required this.status, 10 | required this.openingDays, 11 | required this.openingHours, 12 | }); 13 | 14 | BarbershopRegisterState.initial() 15 | : status = BarbershopRegisterStateStatus.initial, 16 | openingDays = [], 17 | openingHours = []; 18 | 19 | final BarbershopRegisterStateStatus status; 20 | final List openingDays; 21 | final List openingHours; 22 | 23 | BarbershopRegisterState copyWith({ 24 | BarbershopRegisterStateStatus? status, 25 | List? openingDays, 26 | List? openingHours, 27 | }) => 28 | BarbershopRegisterState( 29 | status: status ?? this.status, 30 | openingDays: openingDays ?? this.openingDays, 31 | openingHours: openingHours ?? this.openingHours, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/features/employee/schedule/appointment_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/models/schedule_model.dart'; 3 | import 'package:syncfusion_flutter_calendar/calendar.dart'; 4 | 5 | class AppointmentDataSource extends CalendarDataSource { 6 | AppointmentDataSource({ 7 | required this.schedules, 8 | }); 9 | 10 | final List schedules; 11 | 12 | @override 13 | List? get appointments => schedules.map((e) { 14 | final ScheduleModel( 15 | date: DateTime(:year, :month, :day), 16 | :hour, 17 | :clientName, 18 | ) = e; 19 | 20 | final startTime = DateTime(year, month, day, hour, 0, 0); 21 | final endTime = DateTime(year, month, day, hour + 1, 0, 0); 22 | 23 | return Appointment( 24 | color: AppColors.brown, 25 | startTime: startTime, 26 | endTime: endTime, 27 | subject: clientName, 28 | ); 29 | }).toList(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/models/barbershop_model.dart: -------------------------------------------------------------------------------- 1 | final class BarbershopModel { 2 | const BarbershopModel({ 3 | required this.id, 4 | required this.name, 5 | required this.email, 6 | required this.openDays, 7 | required this.openHours, 8 | }); 9 | 10 | factory BarbershopModel.fromMap(Map json) { 11 | return switch (json) { 12 | { 13 | 'id': int id, 14 | 'name': String name, 15 | 'email': String email, 16 | 'opening_days': final List openDays, 17 | 'opening_hours': final List openHours, 18 | } => 19 | BarbershopModel( 20 | id: id, 21 | name: name, 22 | email: email, 23 | openDays: openDays.cast(), 24 | openHours: openHours.cast(), 25 | ), 26 | _ => throw ArgumentError('Invalid BarbershopModel JSON: $json'), 27 | }; 28 | } 29 | 30 | final int id; 31 | final String name; 32 | final String email; 33 | final List openDays; 34 | final List openHours; 35 | } 36 | -------------------------------------------------------------------------------- /.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: f468f3366c26a5092eb964a230ce7892fda8f2f8 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: f468f3366c26a5092eb964a230ce7892fda8f2f8 17 | base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 18 | - platform: android 19 | create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 20 | base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/user/user_register_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_register_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$userRegisterVMHash() => r'7bcd4f23c5668eff8678e81a961183735f68a823'; 10 | 11 | /// See also [UserRegisterVM]. 12 | @ProviderFor(UserRegisterVM) 13 | final userRegisterVMProvider = AutoDisposeNotifierProvider.internal( 15 | UserRegisterVM.new, 16 | name: r'userRegisterVMProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$userRegisterVMHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$UserRegisterVM = AutoDisposeNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 27 | -------------------------------------------------------------------------------- /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/src/models/schedule_model.dart: -------------------------------------------------------------------------------- 1 | class ScheduleModel { 2 | ScheduleModel({ 3 | required this.id, 4 | required this.barbershopId, 5 | required this.userId, 6 | required this.clientName, 7 | required this.date, 8 | required this.hour, 9 | }); 10 | 11 | factory ScheduleModel.fromMap(Map json) { 12 | switch (json) { 13 | case { 14 | 'id': int id, 15 | 'barbershop_id': int barbershopId, 16 | 'user_id': int userId, 17 | 'client_name': String clientName, 18 | 'date': String scheduleDate, 19 | 'time': int hour, 20 | }: 21 | return ScheduleModel( 22 | id: id, 23 | barbershopId: barbershopId, 24 | userId: userId, 25 | clientName: clientName, 26 | date: DateTime.parse(scheduleDate), 27 | hour: hour, 28 | ); 29 | case _: 30 | throw ArgumentError('Invalid JSON: $json'); 31 | } 32 | } 33 | 34 | final int id, barbershopId, userId, hour; 35 | final String clientName; 36 | final DateTime date; 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/features/employee/register/employee_register_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'employee_register_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$employeeRegisterVMHash() => 10 | r'e8096a13e4ad01a629119f9240b386708fc1e7d9'; 11 | 12 | /// See also [EmployeeRegisterVM]. 13 | @ProviderFor(EmployeeRegisterVM) 14 | final employeeRegisterVMProvider = AutoDisposeNotifierProvider< 15 | EmployeeRegisterVM, EmployeeRegisterState>.internal( 16 | EmployeeRegisterVM.new, 17 | name: r'employeeRegisterVMProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$employeeRegisterVMHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$EmployeeRegisterVM = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Felipe Sales 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/user/user_register_providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_register_providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$userRegisterServiceADMHash() => 10 | r'a2a276c226f68e3d2c5318bc5f6cf8fd4d5372a6'; 11 | 12 | /// See also [userRegisterServiceADM]. 13 | @ProviderFor(userRegisterServiceADM) 14 | final userRegisterServiceADMProvider = 15 | AutoDisposeProvider.internal( 16 | userRegisterServiceADM, 17 | name: r'userRegisterServiceADMProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$userRegisterServiceADMHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef UserRegisterServiceADMRef 26 | = AutoDisposeProviderRef; 27 | // ignore_for_file: type=lint 28 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 29 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/barbershop/barbershop_register_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'barbershop_register_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$barbershopRegisterVMHash() => 10 | r'1b87e3d0ce260f8ac1ec0e3361409d4e38971958'; 11 | 12 | /// See also [BarbershopRegisterVM]. 13 | @ProviderFor(BarbershopRegisterVM) 14 | final barbershopRegisterVMProvider = AutoDisposeNotifierProvider< 15 | BarbershopRegisterVM, BarbershopRegisterState>.internal( 16 | BarbershopRegisterVM.new, 17 | name: r'barbershopRegisterVMProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$barbershopRegisterVMHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$BarbershopRegisterVM = AutoDisposeNotifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 28 | -------------------------------------------------------------------------------- /lib/src/features/employee/register/employee_register_state.dart: -------------------------------------------------------------------------------- 1 | enum EmployeeRegisterStateStatus { 2 | initial, 3 | success, 4 | error, 5 | } 6 | 7 | class EmployeeRegisterState { 8 | EmployeeRegisterState.initial() 9 | : this( 10 | status: EmployeeRegisterStateStatus.initial, 11 | registerADM: false, 12 | workDays: [], 13 | workHours: [], 14 | ); 15 | 16 | EmployeeRegisterState({ 17 | required this.status, 18 | required this.registerADM, 19 | required this.workDays, 20 | required this.workHours, 21 | }); 22 | 23 | final EmployeeRegisterStateStatus status; 24 | final bool registerADM; 25 | final List workDays; 26 | final List workHours; 27 | 28 | EmployeeRegisterState copyWith({ 29 | EmployeeRegisterStateStatus? status, 30 | bool? registerADM, 31 | List? workDays, 32 | List? workHours, 33 | }) => 34 | EmployeeRegisterState( 35 | status: status ?? this.status, 36 | registerADM: registerADM ?? this.registerADM, 37 | workDays: workDays ?? this.workDays, 38 | workHours: workHours ?? this.workHours, 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/features/splash/splash_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/core/providers/application_providers.dart'; 3 | import 'package:barbershop/src/models/user_model.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | part 'splash_vm.g.dart'; 8 | 9 | enum SplashState { 10 | initial, 11 | login, 12 | loggedADM, 13 | loggedEmployee, 14 | } 15 | 16 | @riverpod 17 | class SplashVM extends _$SplashVM { 18 | @override 19 | Future build() async { 20 | final sharedPreferences = await SharedPreferences.getInstance(); 21 | 22 | if (sharedPreferences.containsKey(LocalStorageKeys.accessToken)) { 23 | ref.invalidate(getMeProvider); 24 | ref.invalidate(getMyBarbershopProvider); 25 | 26 | try { 27 | final userModel = await ref.watch(getMeProvider.future); 28 | return switch (userModel) { 29 | UserModelADM() => SplashState.loggedADM, 30 | UserModelEmployee() => SplashState.loggedEmployee, 31 | }; 32 | } catch (e) { 33 | return SplashState.login; 34 | } 35 | } 36 | 37 | return SplashState.login; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/user/user_register_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/fp/either.dart'; 2 | import 'package:barbershop/src/core/providers/application_providers.dart'; 3 | import 'package:barbershop/src/features/auth/register/user/user_register_providers.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'user_register_vm.g.dart'; 7 | 8 | enum UserRegisterStateStatus { initial, success, error } 9 | 10 | @riverpod 11 | class UserRegisterVM extends _$UserRegisterVM { 12 | @override 13 | UserRegisterStateStatus build() => UserRegisterStateStatus.initial; 14 | 15 | Future register({ 16 | required String name, 17 | required String email, 18 | required String password, 19 | }) async { 20 | final userRegisterService = ref.watch(userRegisterServiceADMProvider); 21 | 22 | final userData = ( 23 | name: name, 24 | email: email, 25 | password: password, 26 | ); 27 | 28 | final result = await userRegisterService.execute(userData); 29 | 30 | switch (result) { 31 | case Success(): 32 | ref.invalidate(getMeProvider); 33 | state = UserRegisterStateStatus.success; 34 | case Failure(): 35 | state = UserRegisterStateStatus.error; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/repositories/user/user_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/auth_exception.dart'; 2 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 3 | import 'package:barbershop/src/core/fp/either.dart'; 4 | import 'package:barbershop/src/core/fp/nil.dart'; 5 | import 'package:barbershop/src/models/user_model.dart'; 6 | 7 | abstract interface class UserRepository { 8 | Future> login({ 9 | required String email, 10 | required String password, 11 | }); 12 | 13 | Future> me(); 14 | 15 | Future> registerAdmin( 16 | ({String name, String email, String password}) userData, 17 | ); 18 | 19 | Future>> getEmployees( 20 | int barbershopId, 21 | ); 22 | 23 | Future> registerADMAsEmployee( 24 | ({List workDays, List workHours}) userModel, 25 | ); 26 | 27 | Future> registerEmployee( 28 | ({ 29 | int barbershopId, 30 | String name, 31 | String email, 32 | String password, 33 | List workDays, 34 | List workHours, 35 | }) userModel, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/features/employee/schedule/employee_schedule_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/providers/application_providers.dart'; 4 | import 'package:barbershop/src/models/schedule_model.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | part 'employee_schedule_vm.g.dart'; 8 | 9 | @riverpod 10 | class EmployeeScheduleVM extends _$EmployeeScheduleVM { 11 | @override 12 | Future> build(int userId, DateTime date) async => 13 | switch (await _getSchedules(userId, date)) { 14 | Success(value: final schedules) => schedules, 15 | Failure(:final exception) => throw Exception(exception), 16 | }; 17 | 18 | Future changeDate(int userId, DateTime date) async => 19 | state = switch (await _getSchedules(userId, date)) { 20 | Success(value: final schedules) => AsyncData(schedules), 21 | Failure(:final exception) => 22 | AsyncError(Exception(exception), StackTrace.current), 23 | }; 24 | 25 | Future>> _getSchedules( 26 | int userId, 27 | DateTime date, 28 | ) => 29 | ref 30 | .read(scheduleRepositoryProvider) 31 | .findScheduleByDate((userId: userId, date: date)); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/services/user_register_adm/user_service_adm_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/exceptions/service_exception.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/fp/nil.dart'; 4 | import 'package:barbershop/src/repositories/user/user_repository.dart'; 5 | import 'package:barbershop/src/services/user_login/user_login_service.dart'; 6 | import 'package:barbershop/src/services/user_register_adm/user_service_adm.dart'; 7 | 8 | final class UserRegisterServiceADMImpl implements UserRegisterServiceADM { 9 | UserRegisterServiceADMImpl({ 10 | required UserRepository userRepository, 11 | required UserLoginService userService, 12 | }) : _userRepository = userRepository, 13 | _userService = userService; 14 | 15 | final UserRepository _userRepository; 16 | final UserLoginService _userService; 17 | 18 | @override 19 | Future> execute( 20 | ({String name, String email, String password}) userData, 21 | ) async { 22 | final result = await _userRepository.registerAdmin(userData); 23 | 24 | switch (result) { 25 | case Success(): 26 | return _userService.execute( 27 | email: userData.email, 28 | password: userData.password, 29 | ); 30 | case Failure(:final exception): 31 | return Failure(ServiceException(message: exception.message)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/features/home/adm/home_adm_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:asyncstate/asyncstate.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/providers/application_providers.dart'; 4 | import 'package:barbershop/src/features/home/adm/home_adm_state.dart'; 5 | import 'package:barbershop/src/models/barbershop_model.dart'; 6 | import 'package:barbershop/src/models/user_model.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'home_adm_vm.g.dart'; 10 | 11 | @riverpod 12 | class HomeADMVM extends _$HomeADMVM { 13 | @override 14 | Future build() async { 15 | final repository = ref.read(userRepositoryProvider); 16 | final BarbershopModel(id: barbershopId) = 17 | await ref.read(getMyBarbershopProvider.future); 18 | final me = await ref.watch(getMeProvider.future); 19 | 20 | final employeesResult = await repository.getEmployees(barbershopId); 21 | 22 | switch (employeesResult) { 23 | case Success(value: final employeesData): 24 | final employees = []; 25 | if (me case UserModelADM(workDays: _?, workHours: _?)) { 26 | employees.add(me); 27 | } 28 | employees.addAll(employeesData); 29 | return HomeADMState( 30 | status: HomeADMStateStatus.loaded, 31 | employees: employees, 32 | ); 33 | case Failure(): 34 | return HomeADMState( 35 | status: HomeADMStateStatus.error, 36 | employees: [], 37 | ); 38 | } 39 | } 40 | 41 | Future logout() async => ref.watch(logoutProvider.future).asyncLoader(); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/core/rest_client/interceptors/auth_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:barbershop/src/core/constants/local_storage_keys.dart'; 4 | import 'package:barbershop/src/core/ui/barbershop_nav_global_key.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | final class AuthInterceptor extends Interceptor { 10 | @override 11 | Future onRequest( 12 | RequestOptions options, 13 | RequestInterceptorHandler handler, 14 | ) async { 15 | final RequestOptions(:headers, :extra) = options; 16 | headers.remove(HttpHeaders.authorizationHeader); 17 | 18 | if (extra case {'DIO_AUTH_KEY': true}) { 19 | final sharedPreferences = await SharedPreferences.getInstance(); 20 | headers.addAll({ 21 | HttpHeaders.authorizationHeader: 22 | 'Bearer ${sharedPreferences.getString(LocalStorageKeys.accessToken)}', 23 | }); 24 | } 25 | 26 | return handler.next(options); 27 | } 28 | 29 | @override 30 | Future onError( 31 | DioException err, 32 | ErrorInterceptorHandler handler, 33 | ) async { 34 | super.onError(err, handler); 35 | final DioException(requestOptions: RequestOptions(:extra), :response) = err; 36 | if (extra case {'DIO_AUTH_KEY': true}) { 37 | if (response != null && response.statusCode == HttpStatus.forbidden) { 38 | Navigator.of( 39 | BarbershopNavGlobalKey.instance.navKey.currentContext!, 40 | ).pushNamedAndRemoveUntil('/auth/login', ((route) => false)); 41 | } 42 | } 43 | return handler.reject(err); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/features/auth/login/login_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:asyncstate/asyncstate.dart'; 2 | import 'package:barbershop/src/core/exceptions/service_exception.dart'; 3 | import 'package:barbershop/src/core/fp/either.dart'; 4 | import 'package:barbershop/src/core/providers/application_providers.dart'; 5 | import 'package:barbershop/src/features/auth/login/login_state.dart'; 6 | import 'package:barbershop/src/models/user_model.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'login_vm.g.dart'; 10 | 11 | @riverpod 12 | class LoginVM extends _$LoginVM { 13 | @override 14 | LoginState build() => const LoginState.initial(); 15 | 16 | Future login(String email, String password) async { 17 | final loaderHandler = AsyncLoaderHandler()..start(); 18 | final loginService = ref.watch(userLoginServiceProvider); 19 | 20 | final result = await loginService.execute( 21 | email: email, 22 | password: password, 23 | ); 24 | 25 | switch (result) { 26 | case Success(): 27 | ref 28 | ..invalidate(getMeProvider) 29 | ..invalidate(getMyBarbershopProvider); 30 | final userModel = await ref.read(getMeProvider.future); 31 | state.copyWith( 32 | status: switch (userModel) { 33 | UserModelADM() => LoginStateStatus.admLogin, 34 | UserModelEmployee() => LoginStateStatus.employeeLogin, 35 | }, 36 | ); 37 | 38 | case Failure(exception: ServiceException(:final message)): 39 | state = state.copyWith( 40 | status: LoginStateStatus.error, 41 | errorMessage: () => message, 42 | ); 43 | } 44 | 45 | loaderHandler.close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/core/ui/widgets/avatar_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/core/ui/barbershop_icons.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class AvatarWidget extends StatelessWidget { 6 | const AvatarWidget({super.key}) : hideUploadButton = false; 7 | const AvatarWidget.withoutButton({super.key}) : hideUploadButton = true; 8 | 9 | final bool hideUploadButton; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SizedBox.square( 14 | dimension: 102, 15 | child: Stack( 16 | clipBehavior: Clip.none, 17 | children: [ 18 | Container( 19 | width: 92, 20 | height: 92, 21 | decoration: const BoxDecoration( 22 | image: DecorationImage( 23 | image: AssetImage(AppImages.avatar), 24 | ), 25 | ), 26 | ), 27 | Positioned( 28 | right: -4, 29 | bottom: -4, 30 | child: Offstage( 31 | offstage: hideUploadButton, 32 | child: Container( 33 | decoration: BoxDecoration( 34 | shape: BoxShape.circle, 35 | color: Colors.white, 36 | border: Border.all( 37 | color: AppColors.brown, 38 | width: 4, 39 | ), 40 | ), 41 | child: const Icon( 42 | BarbershopIcons.addEmployee, 43 | color: AppColors.brown, 44 | size: 20, 45 | ), 46 | ), 47 | ), 48 | ), 49 | ], 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/services/user_login/user_login_service_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/local_storage_keys.dart'; 2 | import 'package:barbershop/src/core/exceptions/auth_exception.dart'; 3 | import 'package:barbershop/src/core/exceptions/service_exception.dart'; 4 | import 'package:barbershop/src/core/fp/either.dart'; 5 | import 'package:barbershop/src/core/fp/nil.dart'; 6 | import 'package:barbershop/src/repositories/user/user_repository.dart'; 7 | import 'package:barbershop/src/services/user_login/user_login_service.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | 10 | final class UserLoginServiceImpl implements UserLoginService { 11 | UserLoginServiceImpl({ 12 | required UserRepository userRepository, 13 | }) : _userRepository = userRepository; 14 | 15 | final UserRepository _userRepository; 16 | 17 | @override 18 | Future> execute({ 19 | required String email, 20 | required String password, 21 | }) async { 22 | final result = await _userRepository.login( 23 | email: email, 24 | password: password, 25 | ); 26 | switch (result) { 27 | case Success(:final value): 28 | final sharedPreferences = await SharedPreferences.getInstance(); 29 | sharedPreferences.setString(LocalStorageKeys.accessToken, value); 30 | return Success(nil); 31 | case Failure(:final exception): 32 | return switch (exception) { 33 | AuthError() => 34 | Failure(const ServiceException(message: 'Erro ao realizar login')), 35 | AuthUnauthorizedException() => Failure( 36 | const ServiceException(message: 'Login ou senha inválidos'), 37 | ), 38 | }; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/src/core/ui/barbershop_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const _defaultInputBorder = OutlineInputBorder( 5 | borderSide: BorderSide(color: AppColors.grey), 6 | borderRadius: BorderRadius.all(Radius.circular(8)), 7 | ); 8 | 9 | sealed class BarbershopTheme { 10 | static ThemeData themeData = ThemeData( 11 | useMaterial3: true, 12 | fontFamily: AppFonts.fontFamily, 13 | inputDecorationTheme: InputDecorationTheme( 14 | filled: true, 15 | fillColor: Colors.white, 16 | border: _defaultInputBorder, 17 | labelStyle: const TextStyle(color: AppColors.grey), 18 | focusedBorder: _defaultInputBorder, 19 | errorBorder: _defaultInputBorder.copyWith( 20 | borderSide: const BorderSide(color: AppColors.red), 21 | ), 22 | ), 23 | appBarTheme: const AppBarTheme( 24 | iconTheme: IconThemeData(color: AppColors.brown), 25 | centerTitle: true, 26 | backgroundColor: Colors.white, 27 | titleTextStyle: TextStyle( 28 | color: Colors.black, 29 | fontSize: 18, 30 | fontWeight: FontWeight.w500, 31 | fontFamily: AppFonts.fontFamily, 32 | ), 33 | ), 34 | elevatedButtonTheme: ElevatedButtonThemeData( 35 | style: ElevatedButton.styleFrom( 36 | backgroundColor: AppColors.brown, 37 | foregroundColor: Colors.white, 38 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), 39 | ), 40 | ), 41 | outlinedButtonTheme: OutlinedButtonThemeData( 42 | style: ElevatedButton.styleFrom( 43 | foregroundColor: AppColors.brown, 44 | side: const BorderSide(color: AppColors.brown, width: 1), 45 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), 46 | ), 47 | ), 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/barbershop/barbershop_register_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/fp/either.dart'; 2 | import 'package:barbershop/src/core/providers/application_providers.dart'; 3 | import 'package:barbershop/src/features/auth/register/barbershop/barbershop_register_status.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | part 'barbershop_register_vm.g.dart'; 7 | 8 | @riverpod 9 | class BarbershopRegisterVM extends _$BarbershopRegisterVM { 10 | @override 11 | BarbershopRegisterState build() => BarbershopRegisterState.initial(); 12 | 13 | void addOrRemoveOpeningDays(String weekDay) { 14 | final openingDays = state.openingDays; 15 | if (openingDays.contains(weekDay)) { 16 | openingDays.remove(weekDay); 17 | } else { 18 | openingDays.add(weekDay); 19 | } 20 | state = state.copyWith(openingDays: openingDays); 21 | } 22 | 23 | void addOrRemoveOpeningHours(int hour) { 24 | final openingHours = state.openingHours; 25 | if (openingHours.contains(hour)) { 26 | openingHours.remove(hour); 27 | } else { 28 | openingHours.add(hour); 29 | } 30 | state = state.copyWith(openingHours: openingHours); 31 | } 32 | 33 | Future register({ 34 | required String name, 35 | required String email, 36 | }) async { 37 | final repository = ref.watch(barbershopRepositoryProvider); 38 | final BarbershopRegisterState(:openingDays, :openingHours) = state; 39 | 40 | final dto = ( 41 | name: name, 42 | email: email, 43 | openingDays: openingDays, 44 | openingHours: openingHours, 45 | ); 46 | 47 | final registerResult = await repository.save(dto); 48 | 49 | switch (registerResult) { 50 | case Success(): 51 | ref.invalidate(getMyBarbershopProvider); 52 | state = state.copyWith(status: BarbershopRegisterStateStatus.success); 53 | case Failure(): 54 | state = state.copyWith(status: BarbershopRegisterStateStatus.error); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/features/schedule/schedule_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:asyncstate/asyncstate.dart'; 2 | import 'package:barbershop/src/core/fp/either.dart'; 3 | import 'package:barbershop/src/core/providers/application_providers.dart'; 4 | import 'package:barbershop/src/features/schedule/schedule_state.dart'; 5 | import 'package:barbershop/src/models/barbershop_model.dart'; 6 | import 'package:barbershop/src/models/user_model.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'schedule_vm.g.dart'; 10 | 11 | @riverpod 12 | class ScheduleVM extends _$ScheduleVM { 13 | @override 14 | ScheduleState build() => ScheduleState.initial(); 15 | 16 | void timeSelect(int hour) { 17 | if (hour == state.scheduleTime) { 18 | state = state.copyWith(scheduleHour: () => null); 19 | } else { 20 | state = state.copyWith(scheduleHour: () => hour); 21 | } 22 | } 23 | 24 | void dateSelect(DateTime date) { 25 | state = state.copyWith(scheduleDate: () => date); 26 | } 27 | 28 | Future register({ 29 | required UserModel user, 30 | required String clientName, 31 | }) async { 32 | final asyncLoaderHandler = AsyncLoaderHandler()..start(); 33 | final ScheduleState(:scheduleDate, :scheduleTime) = state; 34 | final scheduleRepository = ref.read(scheduleRepositoryProvider); 35 | final BarbershopModel(id: barbershopId) = 36 | await ref.watch(getMyBarbershopProvider.future); 37 | 38 | final dto = ( 39 | barbershopId: barbershopId, 40 | userId: user.id, 41 | clientName: clientName, 42 | date: scheduleDate!, 43 | time: scheduleTime!, 44 | ); 45 | 46 | final scheduleResult = await scheduleRepository.scheduleClient(dto); 47 | 48 | switch (scheduleResult) { 49 | case Success(): 50 | state = state.copyWith(status: ScheduleStateStatus.success); 51 | case Failure(): 52 | state = state.copyWith(status: ScheduleStateStatus.error); 53 | } 54 | 55 | asyncLoaderHandler.close(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/repositories/barbershop/barbershop_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 4 | import 'package:barbershop/src/core/fp/either.dart'; 5 | import 'package:barbershop/src/core/fp/nil.dart'; 6 | import 'package:barbershop/src/core/rest_client/rest_client.dart'; 7 | import 'package:barbershop/src/models/barbershop_model.dart'; 8 | import 'package:barbershop/src/models/user_model.dart'; 9 | import 'package:barbershop/src/repositories/barbershop/barbershop_repository.dart'; 10 | import 'package:dio/dio.dart'; 11 | 12 | class BarbershopRepositoryImpl implements BarbershopRepository { 13 | BarbershopRepositoryImpl({ 14 | required RestClient restClient, 15 | }) : _restClient = restClient; 16 | 17 | final RestClient _restClient; 18 | 19 | @override 20 | Future> getMyBarbershop( 21 | UserModel userModel, 22 | ) async { 23 | switch (userModel) { 24 | case UserModelADM(): 25 | final Response(data: List(first: data)) = await _restClient.auth.get( 26 | '/barbershop', 27 | queryParameters: {'user_id': '#userAuthRef'}, 28 | ); 29 | return Success(BarbershopModel.fromMap(data)); 30 | case UserModelEmployee(): 31 | final Response(:data) = await _restClient.auth.get( 32 | '/barbershop/${userModel.barbershopId}', 33 | ); 34 | return Success(BarbershopModel.fromMap(data)); 35 | } 36 | } 37 | 38 | @override 39 | Future> save( 40 | ({ 41 | String email, 42 | String name, 43 | List openingDays, 44 | List openingHours, 45 | }) data, 46 | ) async { 47 | try { 48 | await _restClient.auth.post( 49 | '/barbershop', 50 | data: { 51 | 'user_id': '#userAuthRef', 52 | 'name': data.name, 53 | 'email': data.email, 54 | 'opening_days': data.openingDays, 55 | 'opening_hours': data.openingHours, 56 | }, 57 | ); 58 | return Success(nil); 59 | } on DioException catch (e, s) { 60 | const errorMessage = 'Erro ao salvar barbearia'; 61 | log(errorMessage, error: e, stackTrace: s); 62 | return Failure(const RepositoryException(message: errorMessage)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /api/postman/Employee.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "13d446bc-d860-4728-9c40-6e8486d3b874", 4 | "name": "Employee", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "741492" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Get Employee", 11 | "request": { 12 | "auth": { 13 | "type": "bearer", 14 | "bearer": [ 15 | { 16 | "key": "token", 17 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOmZhbHNlLCJleHAiOjE2OTE4NTAwMTUsImlhdCI6MTY5MTg0NjQxNSwiaXNzIjoianNvbl9yZXN0X3NlcnZlciIsIm5iZiI6MTY5MTg0NjQxNSwic3ViIjoiNSJ9.rGaA7Gy1mb3S6JmqjPbamUvhfjPP1q55OR7_0zz3cDk", 18 | "type": "string" 19 | } 20 | ] 21 | }, 22 | "method": "GET", 23 | "header": [], 24 | "url": { 25 | "raw": "http://localhost:8080/users?barbershop_id=2", 26 | "protocol": "http", 27 | "host": [ 28 | "localhost" 29 | ], 30 | "port": "8080", 31 | "path": [ 32 | "users" 33 | ], 34 | "query": [ 35 | { 36 | "key": "barbershop_id", 37 | "value": "2" 38 | } 39 | ] 40 | } 41 | }, 42 | "response": [] 43 | }, 44 | { 45 | "name": "Register", 46 | "request": { 47 | "auth": { 48 | "type": "bearer", 49 | "bearer": [ 50 | { 51 | "key": "token", 52 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOmZhbHNlLCJleHAiOjE2OTE4NTAwMTUsImlhdCI6MTY5MTg0NjQxNSwiaXNzIjoianNvbl9yZXN0X3NlcnZlciIsIm5iZiI6MTY5MTg0NjQxNSwic3ViIjoiNSJ9.rGaA7Gy1mb3S6JmqjPbamUvhfjPP1q55OR7_0zz3cDk", 53 | "type": "string" 54 | } 55 | ] 56 | }, 57 | "method": "POST", 58 | "header": [], 59 | "body": { 60 | "mode": "raw", 61 | "raw": "{\r\n \"name\": \"Rodrigo Rahman 1 Employee\",\r\n \"email\": \"rodrigorahman1employee@gmail.com\",\r\n \"password\": \"123123\",\r\n \"profile\": \"EMPLOYEE\",\r\n \"barbershop_id\": 2,\r\n \"work_days\": [\r\n \"Seg\",\r\n \"Qua\"\r\n ],\r\n \"work_hours\": [\r\n 6,\r\n 7,\r\n 8\r\n ]\r\n}", 62 | "options": { 63 | "raw": { 64 | "language": "json" 65 | } 66 | } 67 | }, 68 | "url": { 69 | "raw": "http://localhost:8080/users", 70 | "protocol": "http", 71 | "host": [ 72 | "localhost" 73 | ], 74 | "port": "8080", 75 | "path": [ 76 | "users" 77 | ] 78 | } 79 | }, 80 | "response": [] 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace "com.felipecastrosales.barbershop" 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.felipecastrosales.barbershop" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/barbershop_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:asyncstate/widget/async_state_builder.dart'; 2 | import 'package:barbershop/src/core/ui/barbershop_nav_global_key.dart'; 3 | import 'package:barbershop/src/core/ui/barbershop_theme.dart'; 4 | import 'package:barbershop/src/core/ui/widgets/barbershop_loader.dart'; 5 | import 'package:barbershop/src/features/auth/login/login_page.dart'; 6 | import 'package:barbershop/src/features/auth/register/barbershop/barbershop_register_page.dart'; 7 | import 'package:barbershop/src/features/auth/register/user/user_register_page.dart'; 8 | import 'package:barbershop/src/features/employee/register/employee_register_page.dart'; 9 | import 'package:barbershop/src/features/employee/schedule/employee_schedule_page.dart'; 10 | import 'package:barbershop/src/features/home/adm/home_adm_page.dart'; 11 | import 'package:barbershop/src/features/home/employee/home_employee_page.dart'; 12 | import 'package:barbershop/src/features/schedule/schedule_page.dart'; 13 | import 'package:barbershop/src/features/splash/splash_page.dart'; 14 | import 'package:flutter/material.dart'; 15 | import 'package:flutter_localizations/flutter_localizations.dart'; 16 | 17 | class BarbershopApp extends StatelessWidget { 18 | const BarbershopApp({super.key}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return AsyncStateBuilder( 23 | customLoader: const BarbershopLoader(), 24 | builder: (asyncNavigatorObserver) { 25 | return MaterialApp( 26 | navigatorObservers: [asyncNavigatorObserver], 27 | navigatorKey: BarbershopNavGlobalKey.instance.navKey, 28 | theme: BarbershopTheme.themeData, 29 | title: 'Barbershop', 30 | home: const SplashPage(), 31 | routes: { 32 | '/auth/login': (_) => const LoginPage(), 33 | '/auth/register/user': (_) => const UserRegisterPage(), 34 | '/auth/register/barbershop': (_) => const BarbershopRegisterPage(), 35 | '/home/adm': (_) => const HomeADMPage(), 36 | '/home/employee': (_) => const HomeEmployeePage(), 37 | '/employee/register': (_) => const EmployeeRegisterPage(), 38 | '/employee/schedule': (_) => const EmployeeSchedulePage(), 39 | '/schedule': (_) => const SchedulePage(), 40 | }, 41 | locale: const Locale('pt', 'BR'), 42 | supportedLocales: const [Locale('pt', 'BR')], 43 | localizationsDelegates: const [ 44 | GlobalMaterialLocalizations.delegate, 45 | GlobalWidgetsLocalizations.delegate, 46 | GlobalCupertinoLocalizations.delegate, 47 | ], 48 | ); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/repositories/schedule/schedule_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 4 | import 'package:barbershop/src/core/fp/either.dart'; 5 | import 'package:barbershop/src/core/fp/nil.dart'; 6 | import 'package:barbershop/src/core/rest_client/rest_client.dart'; 7 | import 'package:barbershop/src/models/schedule_model.dart'; 8 | import 'package:barbershop/src/repositories/schedule/schedule_repository.dart'; 9 | import 'package:dio/dio.dart'; 10 | 11 | class ScheduleRepositoryImpl implements ScheduleRepository { 12 | ScheduleRepositoryImpl({ 13 | required RestClient restClient, 14 | }) : _restClient = restClient; 15 | 16 | final RestClient _restClient; 17 | 18 | @override 19 | Future> scheduleClient( 20 | ({ 21 | int barbershopId, 22 | String clientName, 23 | DateTime date, 24 | int time, 25 | int userId 26 | }) scheduleData, 27 | ) async { 28 | try { 29 | await _restClient.auth.post( 30 | '/schedules', 31 | data: { 32 | 'barbershop_id': scheduleData.barbershopId, 33 | 'user_id': scheduleData.userId, 34 | 'client_name': scheduleData.clientName, 35 | 'date': scheduleData.date.toIso8601String(), 36 | 'time': scheduleData.time, 37 | }, 38 | ); 39 | return Success(nil); 40 | } on DioException catch (e, s) { 41 | const errorMessage = 'Erro ao registrar agendamento'; 42 | log(errorMessage, error: e, stackTrace: s); 43 | return Failure( 44 | const RepositoryException(message: errorMessage), 45 | ); 46 | } 47 | } 48 | 49 | @override 50 | Future>> findScheduleByDate( 51 | ({DateTime date, int userId}) filter, 52 | ) async { 53 | try { 54 | final Response(:List data) = await _restClient.auth.get( 55 | '/schedules', 56 | queryParameters: { 57 | 'user_id': filter.userId, 58 | 'date': filter.date.toIso8601String(), 59 | }, 60 | ); 61 | return Success(data.map((s) => ScheduleModel.fromMap(s)).toList()); 62 | } on DioException catch (e, s) { 63 | const errorMessage = 'Erro ao buscar agendamento de uma data'; 64 | log(errorMessage, error: e, stackTrace: s); 65 | return Failure( 66 | const RepositoryException(message: errorMessage), 67 | ); 68 | } on ArgumentError catch (e, s) { 69 | const errorMessage = 'Json inválido'; 70 | log(errorMessage, error: e, stackTrace: s); 71 | return Failure( 72 | const RepositoryException(message: errorMessage), 73 | ); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/models/user_model.dart: -------------------------------------------------------------------------------- 1 | sealed class UserModel { 2 | UserModel({ 3 | required this.id, 4 | required this.name, 5 | required this.email, 6 | this.avatar, 7 | }); 8 | 9 | factory UserModel.fromMap(Map json) { 10 | return switch (json['profile']) { 11 | 'ADM' => UserModelADM.fromMap(json), 12 | 'EMPLOYEE' => UserModelEmployee.fromMap(json), 13 | _ => throw ArgumentError('User profile not found'), 14 | }; 15 | } 16 | 17 | final int id; 18 | final String name, email; 19 | final String? avatar; 20 | } 21 | 22 | class UserModelADM extends UserModel { 23 | UserModelADM({ 24 | required super.id, 25 | required super.name, 26 | required super.email, 27 | super.avatar, 28 | this.workDays, 29 | this.workHours, 30 | }); 31 | 32 | factory UserModelADM.fromMap(Map json) { 33 | return switch (json) { 34 | { 35 | 'id': final int id, 36 | 'name': final String name, 37 | 'email': final String email, 38 | } => 39 | UserModelADM( 40 | id: id, 41 | name: name, 42 | email: email, 43 | avatar: json['avatar'], 44 | // ignore: avoid_dynamic_calls 45 | workDays: json['work_days']?.cast(), 46 | // ignore: avoid_dynamic_calls 47 | workHours: json['work_hours']?.cast(), 48 | ), 49 | _ => throw ArgumentError('Invalid Json'), 50 | }; 51 | } 52 | 53 | final List? workDays; 54 | final List? workHours; 55 | } 56 | 57 | class UserModelEmployee extends UserModel { 58 | UserModelEmployee({ 59 | required super.id, 60 | required super.name, 61 | required super.email, 62 | required this.barbershopId, 63 | super.avatar, 64 | required this.workDays, 65 | required this.workHours, 66 | }); 67 | 68 | factory UserModelEmployee.fromMap(Map json) { 69 | return switch (json) { 70 | { 71 | 'id': final int id, 72 | 'name': final String name, 73 | 'email': final String email, 74 | 'barbershop_id': final int barbershopId, 75 | 'work_days': final List workDays, 76 | 'work_hours': final List workHours, 77 | } => 78 | UserModelEmployee( 79 | id: id, 80 | name: name, 81 | email: email, 82 | workDays: workDays.cast(), 83 | workHours: workHours.cast(), 84 | avatar: json['avatar'], 85 | barbershopId: barbershopId, 86 | ), 87 | _ => throw ArgumentError('Invalid UserModelEmployee JSON: $json'), 88 | }; 89 | } 90 | 91 | final List workDays; 92 | final List workHours; 93 | final int barbershopId; 94 | } 95 | -------------------------------------------------------------------------------- /api/postman/Auth.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "f76fed62-61f6-402d-afbb-6f0bf5f424a3", 4 | "name": "Auth", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "741492" 7 | }, 8 | "item": [ 9 | { 10 | "name": "login", 11 | "request": { 12 | "method": "POST", 13 | "header": [], 14 | "body": { 15 | "mode": "raw", 16 | "raw": "{\r\n \"email\": \"rodrigorahman1@gmail.com\",\r\n \"password\": \"123123\"\r\n}", 17 | "options": { 18 | "raw": { 19 | "language": "json" 20 | } 21 | } 22 | }, 23 | "url": { 24 | "raw": "http://192.168.15.103:8080/auth", 25 | "protocol": "http", 26 | "host": [ 27 | "192", 28 | "168", 29 | "15", 30 | "103" 31 | ], 32 | "port": "8080", 33 | "path": [ 34 | "auth" 35 | ] 36 | } 37 | }, 38 | "response": [] 39 | }, 40 | { 41 | "name": "register", 42 | "request": { 43 | "method": "POST", 44 | "header": [], 45 | "body": { 46 | "mode": "raw", 47 | "raw": "{\r\n \"name\": \"Rodrigo Silva Rahman de Almeida\",\r\n \"email\": \"rodrigorahman123@gmail.com\",\r\n \"password\": \"123\"\r\n}", 48 | "options": { 49 | "raw": { 50 | "language": "json" 51 | } 52 | } 53 | }, 54 | "url": { 55 | "raw": "http://192.168.15.103:8080/users", 56 | "protocol": "http", 57 | "host": [ 58 | "192", 59 | "168", 60 | "15", 61 | "103" 62 | ], 63 | "port": "8080", 64 | "path": [ 65 | "users" 66 | ] 67 | } 68 | }, 69 | "response": [] 70 | }, 71 | { 72 | "name": "me", 73 | "protocolProfileBehavior": { 74 | "disableBodyPruning": true 75 | }, 76 | "request": { 77 | "auth": { 78 | "type": "bearer", 79 | "bearer": [ 80 | { 81 | "key": "token", 82 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOmZhbHNlLCJleHAiOjE2OTE2NzQxMTAsImlhdCI6MTY5MTY3MDUxMCwiaXNzIjoianNvbl9yZXN0X3NlcnZlciIsIm5iZiI6MTY5MTY3MDUxMCwic3ViIjoiNSJ9.4PyYsDqyIlDZQiWxvTcsDVDqTXrQvtz0i-_F340QowQ", 83 | "type": "string" 84 | } 85 | ] 86 | }, 87 | "method": "GET", 88 | "header": [], 89 | "body": { 90 | "mode": "raw", 91 | "raw": "", 92 | "options": { 93 | "raw": { 94 | "language": "json" 95 | } 96 | } 97 | }, 98 | "url": { 99 | "raw": "http://192.168.15.103:8080/me", 100 | "protocol": "http", 101 | "host": [ 102 | "192", 103 | "168", 104 | "15", 105 | "103" 106 | ], 107 | "port": "8080", 108 | "path": [ 109 | "me" 110 | ] 111 | } 112 | }, 113 | "response": [] 114 | } 115 | ] 116 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: barbershop 2 | description: Barbershop app 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.0.6 <4.0.0' 8 | 9 | dependencies: 10 | asyncstate: ^2.0.3 11 | dio: ^5.3.2 12 | flutter: 13 | sdk: flutter 14 | flutter_localizations: 15 | sdk: flutter 16 | flutter_riverpod: 2.3.6 17 | intl: ^0.18.1 18 | loading_animation_widget: ^1.2.0+4 19 | pretty_dio_logger: ^1.3.1 20 | riverpod_annotation: ^2.1.1 21 | shared_preferences: ^2.2.0 22 | syncfusion_flutter_calendar: ^22.2.11 23 | table_calendar: ^3.0.9 24 | top_snackbar_flutter: ^3.1.0 25 | validatorless: ^1.2.3 26 | 27 | dev_dependencies: 28 | build_runner: ^2.4.6 29 | custom_lint: ^0.5.1 30 | flutter_lints: ^2.0.2 31 | flutter_test: 32 | sdk: flutter 33 | riverpod_generator: 2.2.5 34 | riverpod_lint: 2.0.0 35 | 36 | flutter: 37 | uses-material-design: true 38 | assets: 39 | - assets/images/ 40 | fonts: 41 | - family: barbershop_icons 42 | fonts: 43 | - asset: assets/fonts/barbershop_icons.ttf 44 | - family: Poppins 45 | fonts: 46 | - asset: assets/fonts/Poppins/Poppins-Italic.ttf 47 | style: italic 48 | - asset: assets/fonts/Poppins/Poppins-Regular.ttf 49 | weight: 400 50 | - asset: assets/fonts/Poppins/Poppins-Black.ttf 51 | weight: 900 52 | - asset: assets/fonts/Poppins/Poppins-BlackItalic.ttf 53 | weight: 900 54 | style: italic 55 | - asset: assets/fonts/Poppins/Poppins-Bold.ttf 56 | weight: 700 57 | - asset: assets/fonts/Poppins/Poppins-BoldItalic.ttf 58 | weight: 700 59 | style: italic 60 | - asset: assets/fonts/Poppins/Poppins-ExtraBold.ttf 61 | weight: 800 62 | - asset: assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf 63 | weight: 800 64 | style: italic 65 | - asset: assets/fonts/Poppins/Poppins-ExtraLight.ttf 66 | weight: 200 67 | - asset: assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf 68 | weight: 200 69 | style: italic 70 | - asset: assets/fonts/Poppins/Poppins-Light.ttf 71 | weight: 200 72 | - asset: assets/fonts/Poppins/Poppins-LightItalic.ttf 73 | weight: 200 74 | style: italic 75 | - asset: assets/fonts/Poppins/Poppins-Medium.ttf 76 | weight: 500 77 | - asset: assets/fonts/Poppins/Poppins-MediumItalic.ttf 78 | weight: 500 79 | style: italic 80 | - asset: assets/fonts/Poppins/Poppins-SemiBold.ttf 81 | weight: 600 82 | - asset: assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf 83 | weight: 600 84 | style: italic 85 | - asset: assets/fonts/Poppins/Poppins-Thin.ttf 86 | weight: 100 87 | - asset: assets/fonts/Poppins/Poppins-ThinItalic.ttf 88 | weight: 100 89 | style: italic 90 | -------------------------------------------------------------------------------- /lib/src/features/employee/register/employee_register_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:asyncstate/asyncstate.dart'; 2 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 3 | import 'package:barbershop/src/core/fp/either.dart'; 4 | import 'package:barbershop/src/core/fp/nil.dart'; 5 | import 'package:barbershop/src/core/providers/application_providers.dart'; 6 | import 'package:barbershop/src/features/employee/register/employee_register_state.dart'; 7 | import 'package:barbershop/src/models/barbershop_model.dart'; 8 | import 'package:barbershop/src/repositories/user/user_repository.dart'; 9 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 10 | 11 | part 'employee_register_vm.g.dart'; 12 | 13 | @riverpod 14 | class EmployeeRegisterVM extends _$EmployeeRegisterVM { 15 | @override 16 | EmployeeRegisterState build() => EmployeeRegisterState.initial(); 17 | 18 | void setRegisterADM(bool isRegisterADM) { 19 | state = state.copyWith(registerADM: isRegisterADM); 20 | } 21 | 22 | void addOrRemoveWeekDays(String weekDay) { 23 | final EmployeeRegisterState(:workDays) = state; 24 | 25 | if (workDays.contains(weekDay)) { 26 | workDays.remove(weekDay); 27 | } else { 28 | workDays.add(weekDay); 29 | } 30 | 31 | state = state.copyWith(workDays: workDays); 32 | } 33 | 34 | void addOrRemoveWorkHours(int workHour) { 35 | final EmployeeRegisterState(:workHours) = state; 36 | 37 | if (workHours.contains(workHour)) { 38 | workHours.remove(workHour); 39 | } else { 40 | workHours.add(workHour); 41 | } 42 | 43 | state = state.copyWith(workHours: workHours); 44 | } 45 | 46 | Future register({ 47 | String? name, 48 | String? email, 49 | String? password, 50 | }) async { 51 | final EmployeeRegisterState(:registerADM, :workDays, :workHours) = state; 52 | final asyncLoaderHandler = AsyncLoaderHandler()..start(); 53 | 54 | final UserRepository(:registerADMAsEmployee, :registerEmployee) = 55 | ref.read(userRepositoryProvider); 56 | 57 | final Either resultRegister; 58 | 59 | if (registerADM) { 60 | final dto = ( 61 | workDays: workDays, 62 | workHours: workHours, 63 | ); 64 | resultRegister = await registerADMAsEmployee(dto); 65 | } else { 66 | final BarbershopModel(:id) = 67 | await ref.watch(getMyBarbershopProvider.future); 68 | final dto = ( 69 | barbershopId: id, 70 | name: name!, 71 | email: email!, 72 | password: password!, 73 | workDays: workDays, 74 | workHours: workHours, 75 | ); 76 | 77 | resultRegister = await registerEmployee(dto); 78 | } 79 | 80 | state = state.copyWith( 81 | status: switch (resultRegister) { 82 | Success() => EmployeeRegisterStateStatus.success, 83 | Failure() => EmployeeRegisterStateStatus.error, 84 | }, 85 | ); 86 | 87 | asyncLoaderHandler.close(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/features/home/adm/home_adm_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:barbershop/src/core/constants/constants.dart'; 4 | import 'package:barbershop/src/core/providers/application_providers.dart'; 5 | import 'package:barbershop/src/core/ui/barbershop_icons.dart'; 6 | import 'package:barbershop/src/core/ui/widgets/barbershop_loader.dart'; 7 | import 'package:barbershop/src/features/home/adm/home_adm_state.dart'; 8 | import 'package:barbershop/src/features/home/adm/home_adm_vm.dart'; 9 | import 'package:barbershop/src/features/home/adm/widgets/home_employee_tile.dart'; 10 | import 'package:barbershop/src/features/home/widgets/home_header.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 13 | 14 | class HomeADMPage extends ConsumerWidget { 15 | const HomeADMPage({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | final homeState = ref.watch(homeADMVMProvider); 20 | 21 | return Scaffold( 22 | floatingActionButton: FloatingActionButton( 23 | onPressed: () async { 24 | await Navigator.of(context).pushNamed('/employee/register'); 25 | ref.invalidate(getMeProvider); 26 | ref.invalidate(homeADMVMProvider); 27 | }, 28 | shape: const CircleBorder(), 29 | backgroundColor: AppColors.brown, 30 | child: const CircleAvatar( 31 | backgroundColor: Colors.white, 32 | maxRadius: 12, 33 | child: Icon( 34 | BarbershopIcons.addEmployee, 35 | color: AppColors.brown, 36 | ), 37 | ), 38 | ), 39 | body: homeState.when( 40 | loading: () => const BarbershopLoader(), 41 | error: (e, s) { 42 | log('UI Erro ao buscar colaboradores', error: e, stackTrace: s); 43 | return Center( 44 | child: Column( 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | children: [ 47 | const Text( 48 | 'Erro ao carregar página.', 49 | style: TextStyle(color: Colors.black), 50 | ), 51 | TextButton( 52 | onPressed: () { 53 | ref.read(homeADMVMProvider.notifier).logout(); 54 | }, 55 | child: const Text('Deslogar'), 56 | ), 57 | ], 58 | ), 59 | ); 60 | }, 61 | data: (HomeADMState data) => CustomScrollView( 62 | slivers: [ 63 | const SliverToBoxAdapter( 64 | child: HomeHeader(), 65 | ), 66 | SliverList( 67 | delegate: SliverChildBuilderDelegate( 68 | childCount: data.employees.length, 69 | (context, index) => HomeEmployeeTile( 70 | employee: data.employees[index], 71 | ), 72 | ), 73 | ), 74 | ], 75 | ), 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/core/providers/application_providers.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/fp/either.dart'; 2 | import 'package:barbershop/src/core/rest_client/rest_client.dart'; 3 | import 'package:barbershop/src/core/ui/barbershop_nav_global_key.dart'; 4 | import 'package:barbershop/src/models/barbershop_model.dart'; 5 | import 'package:barbershop/src/models/user_model.dart'; 6 | import 'package:barbershop/src/repositories/barbershop/barbershop_repository.dart'; 7 | import 'package:barbershop/src/repositories/barbershop/barbershop_repository_impl.dart'; 8 | import 'package:barbershop/src/repositories/schedule/schedule_repository.dart'; 9 | import 'package:barbershop/src/repositories/schedule/schedule_repository_impl.dart'; 10 | import 'package:barbershop/src/repositories/user/user_repository.dart'; 11 | import 'package:barbershop/src/repositories/user/user_repository_impl.dart'; 12 | import 'package:barbershop/src/services/user_login/user_login_service.dart'; 13 | import 'package:barbershop/src/services/user_login/user_login_service_impl.dart'; 14 | import 'package:flutter/material.dart'; 15 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 16 | import 'package:shared_preferences/shared_preferences.dart'; 17 | 18 | part 'application_providers.g.dart'; 19 | 20 | @Riverpod(keepAlive: true) 21 | RestClient restClient(RestClientRef ref) => RestClient(); 22 | 23 | @Riverpod(keepAlive: true) 24 | UserRepository userRepository(UserRepositoryRef ref) => 25 | UserRepositoryImpl(restClient: ref.read(restClientProvider)); 26 | 27 | @Riverpod(keepAlive: true) 28 | UserLoginService userLoginService(UserLoginServiceRef ref) => 29 | UserLoginServiceImpl(userRepository: ref.read(userRepositoryProvider)); 30 | 31 | @Riverpod(keepAlive: true) 32 | Future getMe(GetMeRef ref) async { 33 | final result = await ref.watch(userRepositoryProvider).me(); 34 | return switch (result) { 35 | Success(value: final userModel) => userModel, 36 | Failure(:final exception) => throw exception, 37 | }; 38 | } 39 | 40 | @Riverpod(keepAlive: true) 41 | BarbershopRepository barbershopRepository(BarbershopRepositoryRef ref) => 42 | BarbershopRepositoryImpl(restClient: ref.watch(restClientProvider)); 43 | 44 | @Riverpod(keepAlive: true) 45 | Future getMyBarbershop(GetMyBarbershopRef ref) async { 46 | final userModel = await ref.watch(getMeProvider.future); 47 | final barbershopRepository = ref.watch(barbershopRepositoryProvider); 48 | final result = await barbershopRepository.getMyBarbershop(userModel); 49 | 50 | return switch (result) { 51 | Success(value: final barbershop) => barbershop, 52 | Failure(:final exception) => throw exception, 53 | }; 54 | } 55 | 56 | @riverpod 57 | Future logout(LogoutRef ref) async { 58 | final sharedPreferences = await SharedPreferences.getInstance(); 59 | sharedPreferences.clear(); 60 | ref.invalidate(getMeProvider); 61 | ref.invalidate(getMyBarbershopProvider); 62 | Navigator.of(BarbershopNavGlobalKey.instance.navKey.currentContext!) 63 | .pushNamedAndRemoveUntil('/auth/login', (route) => false); 64 | } 65 | 66 | @riverpod 67 | ScheduleRepository scheduleRepository(ScheduleRepositoryRef ref) => 68 | ScheduleRepositoryImpl(restClient: ref.read(restClientProvider)); 69 | -------------------------------------------------------------------------------- /lib/src/features/splash/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:barbershop/src/core/constants/constants.dart'; 4 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 5 | import 'package:barbershop/src/features/auth/login/login_page.dart'; 6 | import 'package:barbershop/src/features/splash/splash_vm.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | 10 | final class SplashPage extends ConsumerStatefulWidget { 11 | const SplashPage({super.key}); 12 | 13 | @override 14 | ConsumerState createState() => _SplashPageState(); 15 | } 16 | 17 | final class _SplashPageState extends ConsumerState { 18 | var _scale = 10.0; 19 | var _animationOpacityLogo = 0.0; 20 | 21 | double get _logoAnimationWidth => 150 * _scale; 22 | double get _logoAnimationHeight => 170 * _scale; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | WidgetsBinding.instance.addPostFrameCallback((_) { 28 | setState(() { 29 | _animationOpacityLogo = 1.0; 30 | _scale = 1.0; 31 | }); 32 | }); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | ref.listen(splashVMProvider, (_, state) { 38 | state.whenOrNull( 39 | error: (e, s) { 40 | log('Erro ao validar login', error: e, stackTrace: s); 41 | context.showError('Erro ao validar login'); 42 | Navigator.of(context) 43 | .pushNamedAndRemoveUntil('/auth/login', (_) => false); 44 | }, 45 | data: (data) => switch (data) { 46 | SplashState.loggedADM => Navigator.of(context) 47 | .pushNamedAndRemoveUntil('/home/adm', (_) => false), 48 | SplashState.loggedEmployee => Navigator.of(context) 49 | .pushNamedAndRemoveUntil('/home/employee', (_) => false), 50 | _ => Navigator.of(context) 51 | .pushNamedAndRemoveUntil('/auth/login', (_) => false), 52 | }, 53 | ); 54 | }); 55 | 56 | return Scaffold( 57 | backgroundColor: Colors.black, 58 | body: DecoratedBox( 59 | decoration: const BoxDecoration( 60 | image: DecorationImage( 61 | image: AssetImage(AppImages.backgroundChair), 62 | fit: BoxFit.cover, 63 | opacity: 0.2, 64 | ), 65 | ), 66 | child: Center( 67 | child: AnimatedOpacity( 68 | opacity: _animationOpacityLogo, 69 | curve: Curves.easeIn, 70 | duration: const Duration(seconds: 3), 71 | onEnd: () => Navigator.of(context).pushAndRemoveUntil( 72 | PageRouteBuilder( 73 | pageBuilder: (_, __, ___) => const LoginPage(), 74 | transitionsBuilder: (_, animation, __, child) => 75 | FadeTransition(opacity: animation, child: child), 76 | ), 77 | (_) => false, 78 | ), 79 | child: AnimatedContainer( 80 | width: _logoAnimationWidth, 81 | height: _logoAnimationHeight, 82 | curve: Curves.linearToEaseOut, 83 | duration: const Duration(seconds: 3), 84 | child: Image.asset( 85 | AppImages.imgLogo, 86 | fit: BoxFit.cover, 87 | ), 88 | ), 89 | ), 90 | ), 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /api/database.json: -------------------------------------------------------------------------------- 1 | {"users":[{"id":5,"name":"Felipe","email":"felipe@gmail.com","password":"123123","profile":"ADM","work_days":["Seg","Qua"],"work_hours":[6,7,8]},{"id":6,"name":"Felipe","email":"felipe@gmail.com","password":"123456","profile":"ADM","work_days":["Seg","Ter","Qua","Qui","Sex","Sab","Dom"],"work_hours":[8]},{"id":7,"name":"Tito ","email":"tito@gmail.com","password":"123456","work_days":["Qui","Qua"],"work_hours":[8,13,18],"barbershop_id":3,"profile":"EMPLOYEE"},{"id":8,"name":"Felipe","email":"felipe@gmail.com","password":"123456","profile":"ADM"},{"id":9,"name":"Felipe Sakes","email":"felipesalesgh@gmsil.vom","password":"123456","profile":"ADM","work_days":["Seg","Ter","Qua","Qui","Sex","Sab","Dom"],"work_hours":[6,7,8,9,10,15,14,13,12,11,17,18,19,20,23,22,21,16]},{"id":10,"name":"Novo Funcionário ","email":"trabalha@gmail.com","password":"123456","work_days":["Seg","Ter","Qua","Qui","Sex","Sab","Dom"],"work_hours":[8,13,18,23],"barbershop_id":3,"profile":"EMPLOYEE"},{"id":11,"name":"Felipe Sales Work ","email":"felipesales7@gmail.com","password":"123456","profile":"ADM"},{"id":12,"name":"felipeteugmail.com","email":"eusou@gmail.com","password":"123456","profile":"ADM"},{"id":13,"name":"Felipe Sales ","email":"felipesales@gmail.com","password":"123456","profile":"ADM"},{"id":14,"name":"Work Aí pai","email":"trabalhaai@gmail.com","password":"123456","work_days":["Ter","Qua","Qui","Sex"],"work_hours":[9,10,17,16],"barbershop_id":8,"profile":"EMPLOYEE"}],"adm_users":[],"barbershop":[{"id":2,"user_id":"5","name":"Barber X","email":"barbearx@gmail.com","opening_days":["Seg","Qua","Sab"],"opening_hours":[6,7,8,9,18,19,20,12,13]},{"id":3,"user_id":"6","name":"Barber CR7","email":"cr7barber@gmail.com","opening_days":["Seg","Ter","Qui","Qua","Sex","Sab","Dom"],"opening_hours":[23,18,13,8]},{"id":4,"user_id":"6","name":"Barber CR7","email":"cr7barber@gmail.com","opening_days":["Seg","Ter","Qua","Qui","Sex","Sab"],"opening_hours":[6,7,8,9,15,10,14,11,13,12,17,16,19,20,18,23,22,21]},{"id":5,"user_id":"9","name":"Workai","email":"souwok@gmail.cok","opening_days":["Seg","Ter","Qua","Qui","Sex","Sab","Dom"],"opening_hours":[23,22,21,16,17,12,11,7,8,13,18,14,9,10,15,20,19,6]},{"id":6,"user_id":"11","name":"Bora Lá Work","email":"work@gmail.com","opening_days":["Seg","Ter","Qui","Qua","Sex","Sab","Dom"],"opening_hours":[6,7,9,12,14,11,10,8,13,20,18,19,17,15,22,16,23,21]},{"id":7,"user_id":"12","name":"Testeando","email":"testeando@gmail.com","opening_days":["Ter","Qua","Qui","Sex"],"opening_hours":[7,8,9,12,14,13,19,17,18]},{"id":8,"user_id":"13","name":"Tester","email":"felipeteste@gmail.com","opening_days":["Ter","Qua","Qui","Sex"],"opening_hours":[9,10,15,13,14,12,17,16,11]}],"schedules":[{"id":1,"barbershop_id":2,"user_id":5,"client_name":"cr7","date":"2023-08-09T00:00:00.000Z","time":8},{"id":2,"barbershop_id":5,"user_id":9,"client_name":"Quero um","date":"2023-09-09T00:00:00.000Z","time":23},{"id":3,"barbershop_id":5,"user_id":9,"client_name":"heu","date":"2023-09-11T00:00:00.000Z","time":14},{"id":4,"barbershop_id":5,"user_id":9,"client_name":"teste","date":"2023-09-20T00:00:00.000Z","time":15},{"id":5,"barbershop_id":3,"user_id":10,"client_name":"Cr7","date":"2023-09-26T00:00:00.000Z","time":23},{"id":6,"barbershop_id":3,"user_id":10,"client_name":"Felipe ","date":"2023-09-11T00:00:00.000Z","time":23},{"id":7,"barbershop_id":3,"user_id":10,"client_name":"Tuuu","date":"2023-09-25T00:00:00.000Z","time":8},{"id":8,"barbershop_id":3,"user_id":10,"client_name":"Teste","date":"2023-09-11T00:00:00.000Z","time":23},{"id":9,"barbershop_id":8,"user_id":14,"client_name":"CR7","date":"2023-09-13T00:00:00.000Z","time":17},{"id":10,"barbershop_id":3,"user_id":10,"client_name":"Eu mesmo","date":"2023-09-13T00:00:00.000Z","time":8}]} -------------------------------------------------------------------------------- /lib/src/features/home/employee/home_employee_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_employee_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$getTotalSchedulesTodayHash() => 10 | r'ec991558c2daf303a25cafc26b1006c9065cae31'; 11 | 12 | /// Copied from Dart SDK 13 | class _SystemHash { 14 | _SystemHash._(); 15 | 16 | static int combine(int hash, int value) { 17 | // ignore: parameter_assignments 18 | hash = 0x1fffffff & (hash + value); 19 | // ignore: parameter_assignments 20 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 21 | return hash ^ (hash >> 6); 22 | } 23 | 24 | static int finish(int hash) { 25 | // ignore: parameter_assignments 26 | hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); 27 | // ignore: parameter_assignments 28 | hash = hash ^ (hash >> 11); 29 | return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); 30 | } 31 | } 32 | 33 | typedef GetTotalSchedulesTodayRef = AutoDisposeFutureProviderRef; 34 | 35 | /// See also [getTotalSchedulesToday]. 36 | @ProviderFor(getTotalSchedulesToday) 37 | const getTotalSchedulesTodayProvider = GetTotalSchedulesTodayFamily(); 38 | 39 | /// See also [getTotalSchedulesToday]. 40 | class GetTotalSchedulesTodayFamily extends Family> { 41 | /// See also [getTotalSchedulesToday]. 42 | const GetTotalSchedulesTodayFamily(); 43 | 44 | /// See also [getTotalSchedulesToday]. 45 | GetTotalSchedulesTodayProvider call( 46 | int userId, 47 | ) { 48 | return GetTotalSchedulesTodayProvider( 49 | userId, 50 | ); 51 | } 52 | 53 | @override 54 | GetTotalSchedulesTodayProvider getProviderOverride( 55 | covariant GetTotalSchedulesTodayProvider provider, 56 | ) { 57 | return call( 58 | provider.userId, 59 | ); 60 | } 61 | 62 | static const Iterable? _dependencies = null; 63 | 64 | @override 65 | Iterable? get dependencies => _dependencies; 66 | 67 | static const Iterable? _allTransitiveDependencies = null; 68 | 69 | @override 70 | Iterable? get allTransitiveDependencies => 71 | _allTransitiveDependencies; 72 | 73 | @override 74 | String? get name => r'getTotalSchedulesTodayProvider'; 75 | } 76 | 77 | /// See also [getTotalSchedulesToday]. 78 | class GetTotalSchedulesTodayProvider extends AutoDisposeFutureProvider { 79 | /// See also [getTotalSchedulesToday]. 80 | GetTotalSchedulesTodayProvider( 81 | this.userId, 82 | ) : super.internal( 83 | (ref) => getTotalSchedulesToday( 84 | ref, 85 | userId, 86 | ), 87 | from: getTotalSchedulesTodayProvider, 88 | name: r'getTotalSchedulesTodayProvider', 89 | debugGetCreateSourceHash: 90 | const bool.fromEnvironment('dart.vm.product') 91 | ? null 92 | : _$getTotalSchedulesTodayHash, 93 | dependencies: GetTotalSchedulesTodayFamily._dependencies, 94 | allTransitiveDependencies: 95 | GetTotalSchedulesTodayFamily._allTransitiveDependencies, 96 | ); 97 | 98 | final int userId; 99 | 100 | @override 101 | bool operator ==(Object other) { 102 | return other is GetTotalSchedulesTodayProvider && other.userId == userId; 103 | } 104 | 105 | @override 106 | int get hashCode { 107 | var hash = _SystemHash.combine(0, runtimeType.hashCode); 108 | hash = _SystemHash.combine(hash, userId.hashCode); 109 | 110 | return _SystemHash.finish(hash); 111 | } 112 | } 113 | // ignore_for_file: type=lint 114 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 115 | -------------------------------------------------------------------------------- /lib/src/features/home/adm/widgets/home_employee_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/core/ui/barbershop_icons.dart'; 3 | import 'package:barbershop/src/models/user_model.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class HomeEmployeeTile extends StatelessWidget { 7 | const HomeEmployeeTile({ 8 | super.key, 9 | required this.employee, 10 | }); 11 | 12 | final UserModel employee; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container( 17 | width: MediaQuery.sizeOf(context).width, 18 | margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), 19 | padding: const EdgeInsets.all(8), 20 | decoration: BoxDecoration( 21 | borderRadius: BorderRadius.circular(8), 22 | border: Border.all(color: AppColors.grey), 23 | ), 24 | child: Row( 25 | children: [ 26 | Container( 27 | width: 56, 28 | height: 56, 29 | decoration: BoxDecoration( 30 | color: AppColors.grey, 31 | borderRadius: BorderRadius.circular(8), 32 | image: DecorationImage( 33 | fit: BoxFit.cover, 34 | image: switch (employee.avatar) { 35 | final avatar? => NetworkImage(avatar), 36 | _ => const AssetImage(AppImages.avatar), 37 | } as ImageProvider, 38 | ), 39 | ), 40 | ), 41 | const SizedBox(width: 12), 42 | Expanded( 43 | child: Column( 44 | mainAxisAlignment: MainAxisAlignment.center, 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | Text( 48 | employee.name, 49 | style: const TextStyle( 50 | fontWeight: FontWeight.w500, 51 | fontSize: 16, 52 | ), 53 | ), 54 | const SizedBox(height: 8), 55 | Row( 56 | mainAxisAlignment: MainAxisAlignment.spaceAround, 57 | children: [ 58 | ElevatedButton( 59 | onPressed: () { 60 | Navigator.of(context).pushNamed( 61 | '/schedule', 62 | arguments: employee, 63 | ); 64 | }, 65 | style: ElevatedButton.styleFrom( 66 | maximumSize: const Size(double.infinity, 56), 67 | padding: const EdgeInsets.symmetric(horizontal: 12), 68 | ), 69 | child: const Text('AGENDAR'), 70 | ), 71 | OutlinedButton( 72 | onPressed: () { 73 | Navigator.of(context).pushNamed( 74 | '/employee/schedule', 75 | arguments: employee, 76 | ); 77 | }, 78 | style: OutlinedButton.styleFrom( 79 | padding: const EdgeInsets.symmetric(horizontal: 8), 80 | ), 81 | child: const Text('VER AGENDA'), 82 | ), 83 | const Icon( 84 | BarbershopIcons.penEdit, 85 | color: AppColors.brown, 86 | size: 16, 87 | ), 88 | const Icon( 89 | BarbershopIcons.trash, 90 | color: AppColors.red, 91 | size: 16, 92 | ), 93 | ], 94 | ), 95 | ], 96 | ), 97 | ), 98 | ], 99 | ), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /api/postman/Barbershop.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "12302d33-c305-49b0-a90e-dd8073d5d881", 4 | "name": "Barbershop", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "741492" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Register", 11 | "request": { 12 | "auth": { 13 | "type": "bearer", 14 | "bearer": [ 15 | { 16 | "key": "token", 17 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOmZhbHNlLCJleHAiOjE2OTE0MTg3MjMsImlhdCI6MTY5MTQxNTEyMywiaXNzIjoianNvbl9yZXN0X3NlcnZlciIsIm5iZiI6MTY5MTQxNTEyMywic3ViIjoiMiJ9.grFKSv7PVn_NpU4y-ImguaP2AgCYw51i328vKp6yKHw", 18 | "type": "string" 19 | } 20 | ] 21 | }, 22 | "method": "POST", 23 | "header": [], 24 | "body": { 25 | "mode": "raw", 26 | "raw": "{\r\n \"user_id\": \"#userAuthRef\",\r\n \"name\": \"Barbershop name\",\r\n \"email\": \"babershop@email.com\",\r\n \"opening_days\": [\r\n \"Seg\",\r\n \"Qua\",\r\n \"Sab\"\r\n ],\r\n \"opening_hours\": [\r\n 6,\r\n 7,\r\n 8,\r\n 9,\r\n 18,\r\n 19,\r\n 20,\r\n 12,\r\n 13\r\n ]\r\n}", 27 | "options": { 28 | "raw": { 29 | "language": "json" 30 | } 31 | } 32 | }, 33 | "url": { 34 | "raw": "http://localhost:8080/barbershop", 35 | "protocol": "http", 36 | "host": [ 37 | "localhost" 38 | ], 39 | "port": "8080", 40 | "path": [ 41 | "barbershop" 42 | ] 43 | } 44 | }, 45 | "response": [] 46 | }, 47 | { 48 | "name": "Get Barbershop", 49 | "request": { 50 | "auth": { 51 | "type": "bearer", 52 | "bearer": [ 53 | { 54 | "key": "token", 55 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOmZhbHNlLCJleHAiOjE2OTE4NTAwMTUsImlhdCI6MTY5MTg0NjQxNSwiaXNzIjoianNvbl9yZXN0X3NlcnZlciIsIm5iZiI6MTY5MTg0NjQxNSwic3ViIjoiNSJ9.rGaA7Gy1mb3S6JmqjPbamUvhfjPP1q55OR7_0zz3cDk", 56 | "type": "string" 57 | } 58 | ] 59 | }, 60 | "method": "GET", 61 | "header": [], 62 | "url": { 63 | "raw": "http://localhost:8080/barbershop?user_id=%23userAuthRef", 64 | "protocol": "http", 65 | "host": [ 66 | "localhost" 67 | ], 68 | "port": "8080", 69 | "path": [ 70 | "barbershop" 71 | ], 72 | "query": [ 73 | { 74 | "key": "user_id", 75 | "value": "%23userAuthRef" 76 | } 77 | ] 78 | } 79 | }, 80 | "response": [] 81 | }, 82 | { 83 | "name": "Get Barbershop by Id", 84 | "protocolProfileBehavior": { 85 | "disableBodyPruning": true 86 | }, 87 | "request": { 88 | "auth": { 89 | "type": "bearer", 90 | "bearer": [ 91 | { 92 | "key": "token", 93 | "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG0iOmZhbHNlLCJleHAiOjE2OTE2NzczNjEsImlhdCI6MTY5MTY3Mzc2MSwiaXNzIjoianNvbl9yZXN0X3NlcnZlciIsIm5iZiI6MTY5MTY3Mzc2MSwic3ViIjoiNSJ9.34Hyo3XYpzrUxi8jL_-RYjU0GmrjxOW0SwFEaxGcp9A", 94 | "type": "string" 95 | } 96 | ] 97 | }, 98 | "method": "GET", 99 | "header": [], 100 | "body": { 101 | "mode": "raw", 102 | "raw": "{\r\n \"user_id\": \"#userAuthRef\",\r\n \"name\": \"Barbershop name\",\r\n \"email\": \"babershop@email.com\",\r\n \"opening_days\": [\r\n \"Seg\",\r\n \"Qua\",\r\n \"Sab\"\r\n ],\r\n \"opening_hours\": [\r\n 6,\r\n 7,\r\n 8,\r\n 9,\r\n 18,\r\n 19,\r\n 20,\r\n 12,\r\n 13\r\n ],\r\n}", 103 | "options": { 104 | "raw": { 105 | "language": "json" 106 | } 107 | } 108 | }, 109 | "url": { 110 | "raw": "http://localhost:8080/barbershop/2", 111 | "protocol": "http", 112 | "host": [ 113 | "localhost" 114 | ], 115 | "port": "8080", 116 | "path": [ 117 | "barbershop", 118 | "2" 119 | ] 120 | } 121 | }, 122 | "response": [] 123 | } 124 | ] 125 | } -------------------------------------------------------------------------------- /lib/src/features/auth/register/barbershop/barbershop_register_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/fp/nil.dart'; 2 | import 'package:barbershop/src/core/ui/helpers/form_helper.dart'; 3 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 4 | import 'package:barbershop/src/core/ui/widgets/hours_panel.dart'; 5 | import 'package:barbershop/src/core/ui/widgets/weekdays_panel.dart'; 6 | import 'package:barbershop/src/features/auth/register/barbershop/barbershop_register_status.dart'; 7 | import 'package:barbershop/src/features/auth/register/barbershop/barbershop_register_vm.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 10 | import 'package:validatorless/validatorless.dart'; 11 | 12 | final class BarbershopRegisterPage extends ConsumerStatefulWidget { 13 | const BarbershopRegisterPage({super.key}); 14 | 15 | @override 16 | ConsumerState createState() => 17 | _BarbershopRegisterPageState(); 18 | } 19 | 20 | final class _BarbershopRegisterPageState 21 | extends ConsumerState { 22 | final formKey = GlobalKey(); 23 | final nameController = TextEditingController(); 24 | final emailController = TextEditingController(); 25 | 26 | @override 27 | void dispose() { 28 | nameController.dispose(); 29 | emailController.dispose(); 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | final BarbershopRegisterVM( 36 | :addOrRemoveOpeningDays, 37 | :addOrRemoveOpeningHours, 38 | :register, 39 | ) = ref.watch(barbershopRegisterVMProvider.notifier); 40 | 41 | ref.listen( 42 | barbershopRegisterVMProvider, 43 | (_, state) => switch (state.status) { 44 | BarbershopRegisterStateStatus.initial => nil, 45 | BarbershopRegisterStateStatus.error => 46 | context.showError('Erro ao cadastrar estabelecimento'), 47 | BarbershopRegisterStateStatus.success => Navigator.of(context) 48 | .pushNamedAndRemoveUntil('/home/adm', (_) => false), 49 | }, 50 | ); 51 | 52 | return Scaffold( 53 | appBar: AppBar(title: const Text('Cadastrar estabelecimento')), 54 | body: Form( 55 | key: formKey, 56 | child: ListView( 57 | padding: const EdgeInsets.all(20), 58 | children: [ 59 | const SizedBox(height: 5), 60 | TextFormField( 61 | controller: nameController, 62 | decoration: const InputDecoration(label: Text('Nome')), 63 | onTapOutside: (_) => context.unfocus(), 64 | validator: Validatorless.required('Nome obrigatório'), 65 | ), 66 | const SizedBox(height: 24), 67 | TextFormField( 68 | controller: emailController, 69 | decoration: const InputDecoration(label: Text('E-mail')), 70 | onTapOutside: (_) => context.unfocus(), 71 | keyboardType: TextInputType.emailAddress, 72 | validator: Validatorless.multiple([ 73 | Validatorless.required('E-mail obrigatório'), 74 | Validatorless.email('E-mail inválido'), 75 | ]), 76 | ), 77 | const SizedBox(height: 24), 78 | WeekdaysPanel( 79 | onDayPressed: addOrRemoveOpeningDays, 80 | ), 81 | const SizedBox(height: 24), 82 | HoursPanel( 83 | startTime: 6, 84 | endTime: 23, 85 | onTimePressed: addOrRemoveOpeningHours, 86 | ), 87 | const SizedBox(height: 24), 88 | ElevatedButton( 89 | onPressed: () => switch (formKey.currentState?.validate()) { 90 | false || null => context.showError('Formulário inválido'), 91 | true => register( 92 | name: nameController.text, 93 | email: emailController.text, 94 | ), 95 | }, 96 | style: ElevatedButton.styleFrom( 97 | minimumSize: const Size.fromHeight(56), 98 | ), 99 | child: const Text('Cadastrar estabelecimento'), 100 | ), 101 | ], 102 | ), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/features/schedule/widgets/schedule_calendar.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:table_calendar/table_calendar.dart'; 5 | 6 | class ScheduleCalendar extends StatefulWidget { 7 | const ScheduleCalendar({ 8 | super.key, 9 | required this.cancelPressed, 10 | required this.onOkPressed, 11 | required this.workDays, 12 | }); 13 | 14 | final VoidCallback cancelPressed; 15 | final ValueChanged onOkPressed; 16 | final List workDays; 17 | 18 | @override 19 | State createState() => _ScheduleCalendarState(); 20 | } 21 | 22 | class _ScheduleCalendarState extends State { 23 | DateTime? selectedDay; 24 | late final List weekDaysEnabled; 25 | 26 | int convertWeekDay(String weekDay) => switch (weekDay.toLowerCase()) { 27 | 'seg' => DateTime.monday, 28 | 'ter' => DateTime.tuesday, 29 | 'qua' => DateTime.wednesday, 30 | 'qui' => DateTime.thursday, 31 | 'sex' => DateTime.friday, 32 | 'sab' => DateTime.saturday, 33 | 'dom' => DateTime.sunday, 34 | _ => 0, 35 | }; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | weekDaysEnabled = widget.workDays.map(convertWeekDay).toList(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final ScheduleCalendar(:cancelPressed, :onOkPressed) = widget; 46 | 47 | return Container( 48 | padding: const EdgeInsets.all(8), 49 | decoration: BoxDecoration( 50 | color: const Color(0xffe6e2e9), 51 | borderRadius: BorderRadius.circular(16), 52 | ), 53 | child: Column( 54 | children: [ 55 | TableCalendar( 56 | availableGestures: AvailableGestures.none, 57 | headerStyle: const HeaderStyle(titleCentered: true), 58 | focusedDay: DateTime.now(), 59 | firstDay: DateTime.utc(1970, 1, 1), 60 | lastDay: DateTime.now().add(const Duration(days: 365 * 10)), 61 | calendarFormat: CalendarFormat.month, 62 | locale: 'pt_BR', 63 | availableCalendarFormats: const {CalendarFormat.month: 'Month'}, 64 | enabledDayPredicate: (day) => weekDaysEnabled.contains(day.weekday), 65 | onDaySelected: (selectedDay, focusedDay) { 66 | setState(() { 67 | this.selectedDay = selectedDay; 68 | }); 69 | }, 70 | selectedDayPredicate: (day) => isSameDay(selectedDay, day), 71 | calendarStyle: CalendarStyle( 72 | selectedDecoration: const BoxDecoration( 73 | color: AppColors.brown, 74 | shape: BoxShape.circle, 75 | ), 76 | todayDecoration: BoxDecoration( 77 | color: AppColors.brown.withOpacity(0.4), 78 | shape: BoxShape.circle, 79 | ), 80 | ), 81 | ), 82 | Row( 83 | mainAxisAlignment: MainAxisAlignment.end, 84 | children: [ 85 | TextButton( 86 | onPressed: cancelPressed, 87 | child: const Text( 88 | 'Cancelar', 89 | style: TextStyle( 90 | fontSize: 14, 91 | fontWeight: FontWeight.w500, 92 | color: AppColors.brown, 93 | ), 94 | ), 95 | ), 96 | TextButton( 97 | onPressed: () { 98 | if (selectedDay == null) { 99 | context.showError('Por favor selecione um dia'); 100 | return; 101 | } 102 | onOkPressed(selectedDay!); 103 | }, 104 | child: const Text( 105 | 'OK', 106 | style: TextStyle( 107 | fontSize: 14, 108 | fontWeight: FontWeight.bold, 109 | color: AppColors.brown, 110 | ), 111 | ), 112 | ), 113 | ], 114 | ), 115 | ], 116 | ), 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/features/employee/schedule/employee_schedule_vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'employee_schedule_vm.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$employeeScheduleVMHash() => 10 | r'fcd4fb19f0b66eb54b6763dd5acac6418cf1a08a'; 11 | 12 | /// Copied from Dart SDK 13 | class _SystemHash { 14 | _SystemHash._(); 15 | 16 | static int combine(int hash, int value) { 17 | // ignore: parameter_assignments 18 | hash = 0x1fffffff & (hash + value); 19 | // ignore: parameter_assignments 20 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 21 | return hash ^ (hash >> 6); 22 | } 23 | 24 | static int finish(int hash) { 25 | // ignore: parameter_assignments 26 | hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); 27 | // ignore: parameter_assignments 28 | hash = hash ^ (hash >> 11); 29 | return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); 30 | } 31 | } 32 | 33 | abstract class _$EmployeeScheduleVM 34 | extends BuildlessAutoDisposeAsyncNotifier> { 35 | late final int userId; 36 | late final DateTime date; 37 | 38 | Future> build( 39 | int userId, 40 | DateTime date, 41 | ); 42 | } 43 | 44 | /// See also [EmployeeScheduleVM]. 45 | @ProviderFor(EmployeeScheduleVM) 46 | const employeeScheduleVMProvider = EmployeeScheduleVMFamily(); 47 | 48 | /// See also [EmployeeScheduleVM]. 49 | class EmployeeScheduleVMFamily extends Family>> { 50 | /// See also [EmployeeScheduleVM]. 51 | const EmployeeScheduleVMFamily(); 52 | 53 | /// See also [EmployeeScheduleVM]. 54 | EmployeeScheduleVMProvider call( 55 | int userId, 56 | DateTime date, 57 | ) { 58 | return EmployeeScheduleVMProvider( 59 | userId, 60 | date, 61 | ); 62 | } 63 | 64 | @override 65 | EmployeeScheduleVMProvider getProviderOverride( 66 | covariant EmployeeScheduleVMProvider provider, 67 | ) { 68 | return call( 69 | provider.userId, 70 | provider.date, 71 | ); 72 | } 73 | 74 | static const Iterable? _dependencies = null; 75 | 76 | @override 77 | Iterable? get dependencies => _dependencies; 78 | 79 | static const Iterable? _allTransitiveDependencies = null; 80 | 81 | @override 82 | Iterable? get allTransitiveDependencies => 83 | _allTransitiveDependencies; 84 | 85 | @override 86 | String? get name => r'employeeScheduleVMProvider'; 87 | } 88 | 89 | /// See also [EmployeeScheduleVM]. 90 | class EmployeeScheduleVMProvider extends AutoDisposeAsyncNotifierProviderImpl< 91 | EmployeeScheduleVM, List> { 92 | /// See also [EmployeeScheduleVM]. 93 | EmployeeScheduleVMProvider( 94 | this.userId, 95 | this.date, 96 | ) : super.internal( 97 | () => EmployeeScheduleVM() 98 | ..userId = userId 99 | ..date = date, 100 | from: employeeScheduleVMProvider, 101 | name: r'employeeScheduleVMProvider', 102 | debugGetCreateSourceHash: 103 | const bool.fromEnvironment('dart.vm.product') 104 | ? null 105 | : _$employeeScheduleVMHash, 106 | dependencies: EmployeeScheduleVMFamily._dependencies, 107 | allTransitiveDependencies: 108 | EmployeeScheduleVMFamily._allTransitiveDependencies, 109 | ); 110 | 111 | final int userId; 112 | final DateTime date; 113 | 114 | @override 115 | bool operator ==(Object other) { 116 | return other is EmployeeScheduleVMProvider && 117 | other.userId == userId && 118 | other.date == date; 119 | } 120 | 121 | @override 122 | int get hashCode { 123 | var hash = _SystemHash.combine(0, runtimeType.hashCode); 124 | hash = _SystemHash.combine(hash, userId.hashCode); 125 | hash = _SystemHash.combine(hash, date.hashCode); 126 | 127 | return _SystemHash.finish(hash); 128 | } 129 | 130 | @override 131 | Future> runNotifierBuild( 132 | covariant EmployeeScheduleVM notifier, 133 | ) { 134 | return notifier.build( 135 | userId, 136 | date, 137 | ); 138 | } 139 | } 140 | // ignore_for_file: type=lint 141 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 142 | -------------------------------------------------------------------------------- /lib/src/core/ui/widgets/weekdays_panel.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class WeekdaysPanel extends StatelessWidget { 5 | const WeekdaysPanel({ 6 | super.key, 7 | required this.onDayPressed, 8 | this.enabledDays, 9 | }); 10 | 11 | final ValueChanged onDayPressed; 12 | final List? enabledDays; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return SizedBox( 17 | width: double.infinity, 18 | child: Column( 19 | crossAxisAlignment: CrossAxisAlignment.start, 20 | children: [ 21 | const Text( 22 | 'Selecione os dias da semana', 23 | style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), 24 | ), 25 | const SizedBox(height: 16), 26 | SingleChildScrollView( 27 | scrollDirection: Axis.horizontal, 28 | child: Row( 29 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 30 | children: [ 31 | ButtonDay( 32 | label: 'Seg', 33 | enabledDays: enabledDays, 34 | onDayPressed: onDayPressed, 35 | ), 36 | ButtonDay( 37 | label: 'Ter', 38 | enabledDays: enabledDays, 39 | onDayPressed: onDayPressed, 40 | ), 41 | ButtonDay( 42 | label: 'Qua', 43 | enabledDays: enabledDays, 44 | onDayPressed: onDayPressed, 45 | ), 46 | ButtonDay( 47 | label: 'Qui', 48 | enabledDays: enabledDays, 49 | onDayPressed: onDayPressed, 50 | ), 51 | ButtonDay( 52 | label: 'Sex', 53 | enabledDays: enabledDays, 54 | onDayPressed: onDayPressed, 55 | ), 56 | ButtonDay( 57 | label: 'Sab', 58 | enabledDays: enabledDays, 59 | onDayPressed: onDayPressed, 60 | ), 61 | ButtonDay( 62 | label: 'Dom', 63 | enabledDays: enabledDays, 64 | onDayPressed: onDayPressed, 65 | ), 66 | ], 67 | ), 68 | ), 69 | ], 70 | ), 71 | ); 72 | } 73 | } 74 | 75 | class ButtonDay extends StatefulWidget { 76 | const ButtonDay({ 77 | super.key, 78 | required this.label, 79 | required this.onDayPressed, 80 | this.enabledDays, 81 | }); 82 | 83 | final String label; 84 | final ValueChanged onDayPressed; 85 | final List? enabledDays; 86 | 87 | @override 88 | State createState() => ButtonDayState(); 89 | } 90 | 91 | class ButtonDayState extends State { 92 | var selected = false; 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | final textColor = selected ? Colors.white : AppColors.grey; 97 | var buttonColor = selected ? AppColors.brown : Colors.white; 98 | final buttonBorderColor = selected ? AppColors.brown : AppColors.grey; 99 | 100 | final ButtonDay(:enabledDays, :label) = widget; 101 | 102 | final disableDay = enabledDays != null && !enabledDays.contains(label); 103 | 104 | if (disableDay) { 105 | buttonColor = Colors.grey[400]!; 106 | } 107 | 108 | return Padding( 109 | padding: const EdgeInsets.all(5), 110 | child: InkWell( 111 | onTap: disableDay 112 | ? null 113 | : () { 114 | widget.onDayPressed(label); 115 | setState(() { 116 | selected = !selected; 117 | }); 118 | }, 119 | borderRadius: const BorderRadius.all(Radius.circular(8)), 120 | child: Container( 121 | width: 40, 122 | height: 56, 123 | decoration: BoxDecoration( 124 | color: buttonColor, 125 | border: Border.fromBorderSide(BorderSide(color: buttonBorderColor)), 126 | borderRadius: const BorderRadius.all(Radius.circular(8)), 127 | ), 128 | child: Center( 129 | child: Text( 130 | label, 131 | style: TextStyle( 132 | color: textColor, 133 | fontSize: 12, 134 | fontWeight: FontWeight.w500, 135 | ), 136 | ), 137 | ), 138 | ), 139 | ), 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/features/home/widgets/home_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/core/providers/application_providers.dart'; 3 | import 'package:barbershop/src/core/ui/barbershop_icons.dart'; 4 | import 'package:barbershop/src/core/ui/widgets/barbershop_loader.dart'; 5 | import 'package:barbershop/src/features/home/adm/home_adm_vm.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | 9 | class HomeHeader extends ConsumerWidget { 10 | const HomeHeader({super.key}) : hideFilter = true; 11 | const HomeHeader.withoutFilter({super.key}) : hideFilter = false; 12 | 13 | final bool hideFilter; 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final barberShop = ref.watch(getMyBarbershopProvider); 18 | 19 | return Container( 20 | width: MediaQuery.sizeOf(context).width, 21 | padding: const EdgeInsets.all(16), 22 | decoration: const BoxDecoration( 23 | color: Colors.black, 24 | borderRadius: BorderRadius.only( 25 | bottomLeft: Radius.circular(32), 26 | bottomRight: Radius.circular(32), 27 | ), 28 | image: DecorationImage( 29 | fit: BoxFit.cover, 30 | opacity: 0.5, 31 | image: AssetImage( 32 | AppImages.backgroundChair, 33 | ), 34 | ), 35 | ), 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | children: [ 39 | SizedBox(height: MediaQuery.maybeViewPaddingOf(context)?.top), 40 | barberShop.maybeWhen( 41 | orElse: () => const Center( 42 | child: BarbershopLoader(), 43 | ), 44 | data: (barbershop) => Row( 45 | children: [ 46 | const CircleAvatar( 47 | backgroundColor: Color(0xffbdbdbd), 48 | child: SizedBox.shrink(), 49 | ), 50 | const SizedBox(width: 16), 51 | Expanded( 52 | child: Text( 53 | barbershop.name, 54 | overflow: TextOverflow.ellipsis, 55 | maxLines: 1, 56 | style: const TextStyle( 57 | color: Colors.white, 58 | fontSize: 14, 59 | fontWeight: FontWeight.bold, 60 | ), 61 | ), 62 | ), 63 | const Spacer(), 64 | const Text( 65 | 'Editar', 66 | style: TextStyle( 67 | color: AppColors.brown, 68 | fontSize: 12, 69 | fontWeight: FontWeight.w700, 70 | ), 71 | ), 72 | IconButton( 73 | alignment: Alignment.centerRight, 74 | onPressed: () { 75 | ref.read(homeADMVMProvider.notifier).logout(); 76 | }, 77 | icon: const Icon( 78 | BarbershopIcons.exit, 79 | color: AppColors.brown, 80 | size: 32, 81 | ), 82 | ), 83 | ], 84 | ), 85 | ), 86 | const SizedBox(height: 24), 87 | const Text( 88 | 'Bem-vindo', 89 | style: TextStyle( 90 | color: Colors.white, 91 | fontWeight: FontWeight.w500, 92 | fontSize: 18, 93 | ), 94 | ), 95 | const SizedBox(height: 24), 96 | const Text( 97 | 'Agende um Cliente', 98 | style: TextStyle( 99 | color: Colors.white, 100 | fontWeight: FontWeight.w600, 101 | fontSize: 40, 102 | ), 103 | ), 104 | Offstage( 105 | offstage: hideFilter, 106 | child: const SizedBox(height: 24), 107 | ), 108 | Offstage( 109 | offstage: hideFilter, 110 | child: TextFormField( 111 | decoration: const InputDecoration( 112 | hintText: 'Buscar colaborador', 113 | suffixIcon: Padding( 114 | padding: EdgeInsets.only(right: 24), 115 | child: Icon( 116 | BarbershopIcons.search, 117 | color: AppColors.brown, 118 | ), 119 | ), 120 | ), 121 | ), 122 | ), 123 | ], 124 | ), 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /assets/fonts/Poppins/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /lib/src/features/auth/register/user/user_register_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/fp/nil.dart'; 2 | import 'package:barbershop/src/core/ui/helpers/form_helper.dart'; 3 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 4 | import 'package:barbershop/src/features/auth/register/user/user_register_vm.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:validatorless/validatorless.dart'; 8 | 9 | class UserRegisterPage extends ConsumerStatefulWidget { 10 | const UserRegisterPage({super.key}); 11 | 12 | @override 13 | ConsumerState createState() => _UserRegisterPageState(); 14 | } 15 | 16 | final class _UserRegisterPageState extends ConsumerState { 17 | final formKey = GlobalKey(); 18 | final nameController = TextEditingController(); 19 | final emailController = TextEditingController(); 20 | final passwordController = TextEditingController(); 21 | 22 | @override 23 | void dispose() { 24 | nameController.dispose(); 25 | emailController.dispose(); 26 | passwordController.dispose(); 27 | super.dispose(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | final userRegisterVM = ref.watch(userRegisterVMProvider.notifier); 33 | 34 | ref.listen( 35 | userRegisterVMProvider, 36 | (_, state) => switch (state) { 37 | UserRegisterStateStatus.initial => nil, 38 | UserRegisterStateStatus.success => 39 | Navigator.of(context).pushNamed('/auth/register/barbershop'), 40 | UserRegisterStateStatus.error => 41 | context.showError('Erro ao registrar usuário Administrador'), 42 | }, 43 | ); 44 | 45 | return Scaffold( 46 | appBar: AppBar( 47 | title: const Text('Criar conta'), 48 | ), 49 | body: Form( 50 | key: formKey, 51 | child: ListView( 52 | padding: const EdgeInsets.all(30), 53 | children: [ 54 | const SizedBox(height: 20), 55 | TextFormField( 56 | controller: nameController, 57 | decoration: const InputDecoration(label: Text('Nome')), 58 | onTapOutside: (_) => context.unfocus(), 59 | validator: Validatorless.required('Nome obrigatório'), 60 | ), 61 | const SizedBox(height: 24), 62 | TextFormField( 63 | controller: emailController, 64 | keyboardType: TextInputType.emailAddress, 65 | decoration: const InputDecoration(label: Text('E-mail')), 66 | onTapOutside: (_) => context.unfocus(), 67 | validator: Validatorless.multiple([ 68 | Validatorless.required('E-mail obrigatório'), 69 | Validatorless.email('E-mail inválido'), 70 | ]), 71 | ), 72 | const SizedBox(height: 24), 73 | TextFormField( 74 | controller: passwordController, 75 | decoration: const InputDecoration(label: Text('Senha')), 76 | obscureText: true, 77 | onTapOutside: (_) => context.unfocus(), 78 | validator: Validatorless.multiple([ 79 | Validatorless.required('Senha obrigatória'), 80 | Validatorless.min(6, 'Senha deve ter no mínimo 6 caracteres'), 81 | ]), 82 | ), 83 | const SizedBox(height: 24), 84 | TextFormField( 85 | decoration: const InputDecoration(label: Text('Confirmar senha')), 86 | obscureText: true, 87 | onTapOutside: (_) => context.unfocus(), 88 | validator: Validatorless.multiple([ 89 | Validatorless.required('Senha obrigatória'), 90 | Validatorless.compare( 91 | passwordController, 92 | 'Senhas não conferem', 93 | ), 94 | ]), 95 | ), 96 | const SizedBox(height: 24), 97 | ElevatedButton( 98 | onPressed: () => switch (formKey.currentState?.validate()) { 99 | null || false => context.showError('Formulário inválido'), 100 | true => userRegisterVM.register( 101 | name: nameController.text, 102 | email: emailController.text, 103 | password: passwordController.text, 104 | ), 105 | }, 106 | style: ElevatedButton.styleFrom( 107 | minimumSize: const Size.fromHeight(56), 108 | ), 109 | child: const Text('CRIAR CONTA'), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/core/ui/widgets/hours_panel.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class HoursPanel extends StatefulWidget { 5 | const HoursPanel({ 6 | super.key, 7 | this.enabledTimes, 8 | required this.startTime, 9 | required this.endTime, 10 | required this.onTimePressed, 11 | }) : singleSelection = false; 12 | 13 | const HoursPanel.singleSelection({ 14 | super.key, 15 | this.enabledTimes, 16 | required this.startTime, 17 | required this.endTime, 18 | required this.onTimePressed, 19 | }) : singleSelection = true; 20 | 21 | final List? enabledTimes; 22 | final int startTime, endTime; 23 | final ValueChanged onTimePressed; 24 | final bool singleSelection; 25 | 26 | @override 27 | State createState() => _HoursPanelState(); 28 | } 29 | 30 | class _HoursPanelState extends State { 31 | int? lastSelection; 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | final HoursPanel(:singleSelection) = widget; 36 | 37 | return Column( 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | children: [ 40 | const Text( 41 | 'Selecione os horários de atendimento', 42 | style: TextStyle( 43 | fontSize: 14, 44 | fontWeight: FontWeight.w500, 45 | ), 46 | ), 47 | const SizedBox(height: 16), 48 | Wrap( 49 | spacing: 8, 50 | runSpacing: 16, 51 | children: [ 52 | for (int i = widget.startTime; i <= widget.endTime; i++) 53 | TimeButton( 54 | enabledTimes: widget.enabledTimes, 55 | label: '${i.toString().padLeft(2, '0')}:00', 56 | value: i, 57 | timeSelected: lastSelection, 58 | singleSelection: singleSelection, 59 | onPressed: (timeSelected) { 60 | setState(() { 61 | if (singleSelection) { 62 | if (lastSelection == timeSelected) { 63 | lastSelection = null; 64 | } else { 65 | lastSelection = timeSelected; 66 | } 67 | } 68 | }); 69 | widget.onTimePressed(timeSelected); 70 | }, 71 | ), 72 | ], 73 | ), 74 | ], 75 | ); 76 | } 77 | } 78 | 79 | class TimeButton extends StatefulWidget { 80 | const TimeButton({ 81 | super.key, 82 | this.enabledTimes, 83 | required this.label, 84 | required this.value, 85 | required this.onPressed, 86 | required this.singleSelection, 87 | required this.timeSelected, 88 | }); 89 | 90 | final List? enabledTimes; 91 | final String label; 92 | final int value; 93 | final ValueChanged onPressed; 94 | final bool singleSelection; 95 | final int? timeSelected; 96 | 97 | @override 98 | State createState() => _TimeButtonState(); 99 | } 100 | 101 | class _TimeButtonState extends State { 102 | var selected = false; 103 | 104 | @override 105 | Widget build(BuildContext context) { 106 | final TimeButton( 107 | :singleSelection, 108 | :timeSelected, 109 | :value, 110 | :label, 111 | :enabledTimes, 112 | :onPressed, 113 | ) = widget; 114 | 115 | if (singleSelection && timeSelected != null) { 116 | selected = timeSelected == value; 117 | } 118 | 119 | final textColor = selected ? Colors.white : AppColors.grey; 120 | var buttonColor = selected ? AppColors.brown : Colors.white; 121 | final buttonBorderColor = selected ? AppColors.brown : AppColors.grey; 122 | 123 | final disableTime = enabledTimes != null && !enabledTimes.contains(value); 124 | 125 | if (disableTime) { 126 | buttonColor = Colors.grey[400]!; 127 | } 128 | 129 | return InkWell( 130 | borderRadius: BorderRadius.circular(8), 131 | onTap: disableTime 132 | ? null 133 | : () { 134 | setState(() { 135 | selected = !selected; 136 | onPressed(value); 137 | }); 138 | }, 139 | child: Container( 140 | width: 64, 141 | height: 36, 142 | decoration: BoxDecoration( 143 | borderRadius: BorderRadius.circular(8), 144 | color: buttonColor, 145 | border: Border.all(color: buttonBorderColor), 146 | ), 147 | child: Center( 148 | child: Text( 149 | label, 150 | style: TextStyle( 151 | color: textColor, 152 | fontSize: 12, 153 | fontWeight: FontWeight.w500, 154 | ), 155 | ), 156 | ), 157 | ), 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/src/core/providers/application_providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'application_providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$restClientHash() => r'0ee58f1fd102b2016ed621885f1e8d52ed00da66'; 10 | 11 | /// See also [restClient]. 12 | @ProviderFor(restClient) 13 | final restClientProvider = Provider.internal( 14 | restClient, 15 | name: r'restClientProvider', 16 | debugGetCreateSourceHash: 17 | const bool.fromEnvironment('dart.vm.product') ? null : _$restClientHash, 18 | dependencies: null, 19 | allTransitiveDependencies: null, 20 | ); 21 | 22 | typedef RestClientRef = ProviderRef; 23 | String _$userRepositoryHash() => r'4a324f69804b6738f220b7c48b19aad627021894'; 24 | 25 | /// See also [userRepository]. 26 | @ProviderFor(userRepository) 27 | final userRepositoryProvider = Provider.internal( 28 | userRepository, 29 | name: r'userRepositoryProvider', 30 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 31 | ? null 32 | : _$userRepositoryHash, 33 | dependencies: null, 34 | allTransitiveDependencies: null, 35 | ); 36 | 37 | typedef UserRepositoryRef = ProviderRef; 38 | String _$userLoginServiceHash() => r'62431221aac8e45888e74928ecf0b5836e72b999'; 39 | 40 | /// See also [userLoginService]. 41 | @ProviderFor(userLoginService) 42 | final userLoginServiceProvider = Provider.internal( 43 | userLoginService, 44 | name: r'userLoginServiceProvider', 45 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 46 | ? null 47 | : _$userLoginServiceHash, 48 | dependencies: null, 49 | allTransitiveDependencies: null, 50 | ); 51 | 52 | typedef UserLoginServiceRef = ProviderRef; 53 | String _$getMeHash() => r'835de91f459d1216fe7813de1ce4ffa8c28975d4'; 54 | 55 | /// See also [getMe]. 56 | @ProviderFor(getMe) 57 | final getMeProvider = FutureProvider.internal( 58 | getMe, 59 | name: r'getMeProvider', 60 | debugGetCreateSourceHash: 61 | const bool.fromEnvironment('dart.vm.product') ? null : _$getMeHash, 62 | dependencies: null, 63 | allTransitiveDependencies: null, 64 | ); 65 | 66 | typedef GetMeRef = FutureProviderRef; 67 | String _$barbershopRepositoryHash() => 68 | r'a64ad01ae6b70f8192c02a09abc33ea968315cb9'; 69 | 70 | /// See also [barbershopRepository]. 71 | @ProviderFor(barbershopRepository) 72 | final barbershopRepositoryProvider = Provider.internal( 73 | barbershopRepository, 74 | name: r'barbershopRepositoryProvider', 75 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 76 | ? null 77 | : _$barbershopRepositoryHash, 78 | dependencies: null, 79 | allTransitiveDependencies: null, 80 | ); 81 | 82 | typedef BarbershopRepositoryRef = ProviderRef; 83 | String _$getMyBarbershopHash() => r'e1c72495b6a8e9b1b3af23c7ef1c9144fb45e841'; 84 | 85 | /// See also [getMyBarbershop]. 86 | @ProviderFor(getMyBarbershop) 87 | final getMyBarbershopProvider = FutureProvider.internal( 88 | getMyBarbershop, 89 | name: r'getMyBarbershopProvider', 90 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 91 | ? null 92 | : _$getMyBarbershopHash, 93 | dependencies: null, 94 | allTransitiveDependencies: null, 95 | ); 96 | 97 | typedef GetMyBarbershopRef = FutureProviderRef; 98 | String _$logoutHash() => r'd1382ccaba4d095ba11e122b71da66151bc6fbd6'; 99 | 100 | /// See also [logout]. 101 | @ProviderFor(logout) 102 | final logoutProvider = AutoDisposeFutureProvider.internal( 103 | logout, 104 | name: r'logoutProvider', 105 | debugGetCreateSourceHash: 106 | const bool.fromEnvironment('dart.vm.product') ? null : _$logoutHash, 107 | dependencies: null, 108 | allTransitiveDependencies: null, 109 | ); 110 | 111 | typedef LogoutRef = AutoDisposeFutureProviderRef; 112 | String _$scheduleRepositoryHash() => 113 | r'd8bb90e09ddffd4926259e9e2ec796a76739a37d'; 114 | 115 | /// See also [scheduleRepository]. 116 | @ProviderFor(scheduleRepository) 117 | final scheduleRepositoryProvider = 118 | AutoDisposeProvider.internal( 119 | scheduleRepository, 120 | name: r'scheduleRepositoryProvider', 121 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 122 | ? null 123 | : _$scheduleRepositoryHash, 124 | dependencies: null, 125 | allTransitiveDependencies: null, 126 | ); 127 | 128 | typedef ScheduleRepositoryRef = AutoDisposeProviderRef; 129 | // ignore_for_file: type=lint 130 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member 131 | -------------------------------------------------------------------------------- /lib/src/features/employee/schedule/employee_schedule_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'package:barbershop/src/core/constants/constants.dart'; 3 | import 'package:barbershop/src/core/ui/widgets/barbershop_loader.dart'; 4 | import 'package:barbershop/src/features/employee/schedule/appointment_data_source.dart'; 5 | import 'package:barbershop/src/features/employee/schedule/employee_schedule_vm.dart'; 6 | import 'package:barbershop/src/models/user_model.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 9 | import 'package:intl/intl.dart'; 10 | import 'package:syncfusion_flutter_calendar/calendar.dart'; 11 | 12 | class EmployeeSchedulePage extends ConsumerStatefulWidget { 13 | const EmployeeSchedulePage({super.key}); 14 | 15 | @override 16 | ConsumerState createState() => 17 | _EmployeeSchedulePageState(); 18 | } 19 | 20 | class _EmployeeSchedulePageState extends ConsumerState { 21 | late DateTime dateSelected; 22 | var ignoreFirstLoad = true; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | final DateTime(:year, :month, :day) = DateTime.now(); 28 | dateSelected = DateTime(year, month, day, 0, 0, 0); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final UserModel(id: userId, :name) = 34 | ModalRoute.of(context)?.settings.arguments as UserModel; 35 | 36 | final scheduleAsync = ref.watch( 37 | employeeScheduleVMProvider(userId, dateSelected), 38 | ); 39 | 40 | return Scaffold( 41 | appBar: AppBar( 42 | title: const Text('Agenda'), 43 | ), 44 | body: Column( 45 | children: [ 46 | Text( 47 | name, 48 | style: const TextStyle( 49 | fontSize: 20, 50 | fontWeight: FontWeight.w500, 51 | ), 52 | ), 53 | const SizedBox(height: 44), 54 | scheduleAsync.when( 55 | loading: () => const BarbershopLoader(), 56 | error: (e, s) { 57 | const errorMessage = 'Erro ao carregar agendamento'; 58 | log(errorMessage, error: e, stackTrace: s); 59 | return const Center( 60 | child: Text(errorMessage), 61 | ); 62 | }, 63 | data: (schedules) { 64 | return Expanded( 65 | child: SfCalendar( 66 | allowViewNavigation: true, 67 | view: CalendarView.day, 68 | showNavigationArrow: true, 69 | todayHighlightColor: AppColors.brown, 70 | showDatePickerButton: true, 71 | showTodayButton: true, 72 | dataSource: AppointmentDataSource(schedules: schedules), 73 | onViewChanged: (viewChangedDetails) { 74 | if (ignoreFirstLoad) { 75 | ignoreFirstLoad = false; 76 | return; 77 | } 78 | final employeeSchedule = ref.read( 79 | employeeScheduleVMProvider(userId, dateSelected).notifier, 80 | ); 81 | employeeSchedule.changeDate( 82 | userId, 83 | viewChangedDetails.visibleDates.first, 84 | ); 85 | }, 86 | onTap: (calendarTapDetails) { 87 | if (calendarTapDetails.appointments?.isNotEmpty ?? false) { 88 | showModalBottomSheet( 89 | context: context, 90 | builder: (context) { 91 | final dateFormat = DateFormat('dd//MM/yyyy HH:mm'); 92 | return SizedBox( 93 | height: 200, 94 | child: Center( 95 | child: Column( 96 | mainAxisAlignment: MainAxisAlignment.center, 97 | children: [ 98 | Text( 99 | // ignore: avoid_dynamic_calls 100 | 'Cliente: ${calendarTapDetails.appointments!.first.subject}', 101 | ), 102 | Text( 103 | 'Horário: ${dateFormat.format(calendarTapDetails.date ?? DateTime.now())}', 104 | ), 105 | ], 106 | ), 107 | ), 108 | ); 109 | }, 110 | ); 111 | } 112 | }, 113 | ), 114 | ); 115 | }, 116 | ), 117 | ], 118 | ), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/features/home/employee/home_employee_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:barbershop/src/core/constants/constants.dart'; 4 | import 'package:barbershop/src/core/providers/application_providers.dart'; 5 | import 'package:barbershop/src/core/ui/widgets/avatar_widget.dart'; 6 | import 'package:barbershop/src/core/ui/widgets/barbershop_loader.dart'; 7 | import 'package:barbershop/src/features/home/employee/home_employee_provider.dart'; 8 | import 'package:barbershop/src/features/home/widgets/home_header.dart'; 9 | import 'package:barbershop/src/models/user_model.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 12 | 13 | class HomeEmployeePage extends ConsumerWidget { 14 | const HomeEmployeePage({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | final userModelAsync = ref.watch(getMeProvider); 19 | 20 | return Scaffold( 21 | body: userModelAsync.when( 22 | error: (e, s) { 23 | const errorMessage = 'Erro ao carregar página'; 24 | log(errorMessage, error: e, stackTrace: s); 25 | return const Center( 26 | child: Text(errorMessage), 27 | ); 28 | }, 29 | loading: () => const BarbershopLoader(), 30 | data: (user) { 31 | final UserModel(:id, :name) = user; 32 | return CustomScrollView( 33 | slivers: [ 34 | const SliverToBoxAdapter( 35 | child: HomeHeader(), 36 | ), 37 | SliverFillRemaining( 38 | hasScrollBody: false, 39 | child: Padding( 40 | padding: const EdgeInsets.all(24), 41 | child: Column( 42 | children: [ 43 | const AvatarWidget.withoutButton(), 44 | const SizedBox(height: 24), 45 | Text( 46 | name, 47 | style: const TextStyle( 48 | fontSize: 20, 49 | fontWeight: FontWeight.w500, 50 | ), 51 | ), 52 | const SizedBox(height: 20), 53 | Container( 54 | width: MediaQuery.of(context).size.width * .7, 55 | height: 108, 56 | decoration: BoxDecoration( 57 | border: Border.all(color: AppColors.grey), 58 | borderRadius: BorderRadius.circular(8), 59 | ), 60 | child: Column( 61 | mainAxisAlignment: MainAxisAlignment.center, 62 | children: [ 63 | Consumer( 64 | builder: (context, ref, child) { 65 | final totalAsync = ref.watch( 66 | getTotalSchedulesTodayProvider(id), 67 | ); 68 | 69 | return totalAsync.when( 70 | error: (e, s) { 71 | const errorMessage = 72 | 'Erro ao carregar total de agendamentos'; 73 | return const Text(errorMessage); 74 | }, 75 | loading: () => const BarbershopLoader(), 76 | skipLoadingOnRefresh: false, 77 | data: (totalScheduule) { 78 | return Text( 79 | '$totalScheduule', 80 | style: const TextStyle( 81 | fontSize: 40, 82 | color: AppColors.brown, 83 | fontWeight: FontWeight.w600, 84 | ), 85 | ); 86 | }, 87 | ); 88 | }, 89 | ), 90 | const Text( 91 | 'Hoje', 92 | style: TextStyle( 93 | fontSize: 16, 94 | color: AppColors.brown, 95 | fontWeight: FontWeight.w600, 96 | ), 97 | ), 98 | ], 99 | ), 100 | ), 101 | const SizedBox(height: 24), 102 | ElevatedButton( 103 | onPressed: () async { 104 | await Navigator.of(context).pushNamed( 105 | '/schedule', 106 | arguments: user, 107 | ); 108 | ref.invalidate(getTotalSchedulesTodayProvider(id)); 109 | }, 110 | style: ElevatedButton.styleFrom( 111 | minimumSize: const Size.fromHeight(56), 112 | ), 113 | child: const Text('AGENDAR CLIENTE'), 114 | ), 115 | const SizedBox(height: 24), 116 | OutlinedButton( 117 | style: ElevatedButton.styleFrom( 118 | minimumSize: const Size.fromHeight(56), 119 | ), 120 | onPressed: () { 121 | Navigator.of(context).pushNamed( 122 | '/employee/schedule', 123 | arguments: user, 124 | ); 125 | }, 126 | child: const Text('VER AGENDA'), 127 | ), 128 | ], 129 | ), 130 | ), 131 | ), 132 | ], 133 | ); 134 | }, 135 | ), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/src/repositories/user/user_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:io'; 3 | 4 | import 'package:barbershop/src/core/exceptions/auth_exception.dart'; 5 | import 'package:barbershop/src/core/exceptions/repository_exception.dart'; 6 | import 'package:barbershop/src/core/fp/either.dart'; 7 | import 'package:barbershop/src/core/fp/nil.dart'; 8 | import 'package:barbershop/src/core/rest_client/rest_client.dart'; 9 | import 'package:barbershop/src/models/user_model.dart'; 10 | import 'package:barbershop/src/repositories/user/user_repository.dart'; 11 | import 'package:dio/dio.dart'; 12 | 13 | class UserRepositoryImpl implements UserRepository { 14 | const UserRepositoryImpl({ 15 | required RestClient restClient, 16 | }) : _restClient = restClient; 17 | 18 | final RestClient _restClient; 19 | 20 | @override 21 | Future> login({ 22 | required String email, 23 | required String password, 24 | }) async { 25 | try { 26 | final Response(:data) = await _restClient.unAuth.post( 27 | '/auth', 28 | data: {'email': email, 'password': password}, 29 | ); 30 | // ignore: avoid_dynamic_calls 31 | return Success(data['access_token']); 32 | } on DioException catch (e, s) { 33 | if (e.response != null) { 34 | final Response(:statusCode) = e.response!; 35 | if (statusCode == HttpStatus.forbidden) { 36 | log('Login ou senha inválido login', error: e, stackTrace: s); 37 | return Failure(const AuthUnauthorizedException()); 38 | } 39 | } 40 | log('Erro ao realizar login', error: e, stackTrace: s); 41 | return Failure(const AuthError(message: 'Erro ao realizar login')); 42 | } 43 | } 44 | 45 | @override 46 | Future> me() async { 47 | try { 48 | final Response(:data) = await _restClient.auth.get('/me'); 49 | return Success(UserModel.fromMap(data)); 50 | } on DioException catch (e, s) { 51 | log('Erro ao buscar usuário logado', error: e, stackTrace: s); 52 | return Failure( 53 | const RepositoryException(message: 'Erro ao buscar usuário logado'), 54 | ); 55 | } on ArgumentError catch (e, s) { 56 | log('Invalid JSON', error: e, stackTrace: s); 57 | return Failure( 58 | const RepositoryException(message: 'Invalid JSON'), 59 | ); 60 | } 61 | } 62 | 63 | @override 64 | Future> registerAdmin( 65 | ({String email, String name, String password}) userData, 66 | ) async { 67 | try { 68 | await _restClient.unAuth.post( 69 | '/users', 70 | data: { 71 | 'name': userData.name, 72 | 'email': userData.email, 73 | 'password': userData.password, 74 | 'profile': 'ADM', 75 | }, 76 | ); 77 | return Success(nil); 78 | } on DioException catch (e, s) { 79 | log('Erro ao registrar usuário', error: e, stackTrace: s); 80 | return Failure( 81 | const RepositoryException( 82 | message: 'Erro ao registrar usuário adminstrador', 83 | ), 84 | ); 85 | } 86 | } 87 | 88 | @override 89 | Future>> getEmployees( 90 | int barbershopId, 91 | ) async { 92 | try { 93 | final Response(:List data) = await _restClient.auth.get( 94 | '/users', 95 | queryParameters: { 96 | 'barbershop_id': barbershopId, 97 | }, 98 | ); 99 | final employees = data.map((e) => UserModelEmployee.fromMap(e)).toList(); 100 | return Success(employees); 101 | } on DioException catch (e, s) { 102 | const errorMessage = 'Erro ao buscar colaboradores'; 103 | log(errorMessage, error: e, stackTrace: s); 104 | return Failure(const RepositoryException(message: errorMessage)); 105 | } on ArgumentError catch (e, s) { 106 | const errorMessage = 'Erro ao buscar colaboradores (Invalid JSON)'; 107 | log(errorMessage, error: e, stackTrace: s); 108 | return Failure(const RepositoryException(message: errorMessage)); 109 | } 110 | } 111 | 112 | @override 113 | Future> registerADMAsEmployee( 114 | ({List workHours, List workDays}) userModel, 115 | ) async { 116 | try { 117 | final userModelResult = await me(); 118 | 119 | final int userId; 120 | 121 | switch (userModelResult) { 122 | case Success(value: UserModel(:var id)): 123 | userId = id; 124 | case Failure(:var exception): 125 | return Failure(exception); 126 | } 127 | 128 | await _restClient.auth.put( 129 | '/users/$userId', 130 | data: { 131 | 'work_days': userModel.workDays, 132 | 'work_hours': userModel.workHours, 133 | }, 134 | ); 135 | 136 | return Success(nil); 137 | } on DioException catch (e, s) { 138 | const errorMessage = 'Erro ao inserir administrador como colaborador'; 139 | log(errorMessage, error: e, stackTrace: s); 140 | return Failure( 141 | const RepositoryException(message: errorMessage), 142 | ); 143 | } 144 | } 145 | 146 | @override 147 | Future> registerEmployee( 148 | ({ 149 | int barbershopId, 150 | String email, 151 | String name, 152 | String password, 153 | List workDays, 154 | List workHours 155 | }) userModel, 156 | ) async { 157 | try { 158 | await _restClient.auth.post( 159 | '/users/', 160 | data: { 161 | 'name': userModel.name, 162 | 'email': userModel.email, 163 | 'password': userModel.password, 164 | 'work_days': userModel.workDays, 165 | 'work_hours': userModel.workHours, 166 | 'barbershop_id': userModel.barbershopId, 167 | 'profile': 'EMPLOYEE', 168 | }, 169 | ); 170 | 171 | return Success(nil); 172 | } on DioException catch (e, s) { 173 | const errorMessage = 'Erro ao inserir colaborador'; 174 | log(errorMessage, error: e, stackTrace: s); 175 | return Failure( 176 | const RepositoryException(message: errorMessage), 177 | ); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo Barbershop App 3 |

4 | 5 |

Barbershop App

6 | 7 | --- 8 | 9 |

Topics 📋

10 | 11 | - [📖 Sobre](#-about) 12 | - [📱 Preview](#-preview) 13 | - [📦 Assets](#-assets) 14 | - [🛠️ Features and Technologies Studied](#-features-and-technologies-studied) 15 | - [🤯 Challenges and Learning along the way](#-challenges-and-learning-along-the-way) 16 | - [🤔 How to use](#-how-to-use) 17 | - [💪 How to contribute](#-how-to-contribute) 18 | - [📝 License](#-license) 19 | 20 | --- 21 | 22 |

📖 About

23 | 24 | An app for barber shop professionals, where it is possible to manage your own barber shop and even yourself. In addition to having the flow of scheduling appointments and controlling services. 25 | 26 | --- 27 | 28 |

📱 Preview

29 | 30 | https://github.com/felipecastrosales/barbershop/assets/59374587/ff81c19d-851f-4148-9612-bb08fe8a7fb8 31 | 32 | --- 33 | 34 |

📦 Assets

35 | 36 | - **`Postman Collections`**. 37 | - **`Figma`**; 38 | 39 | 40 | --- 41 | 42 |

🛠️ Features and Technologies Studied

43 | 44 | - Using Dart 3 powers: 45 | - Functional Programming (Either) with Pure Dart (using Dart 3 power); 46 | - Using a lot `switch` ways; 47 | - Using `pattern matching` etc. 48 | - Postman Collections; 49 | - Themes, help extensions, constants helpers, custom exceptions and more; 50 | - Rest Client with Dio and Interceptors; 51 | - Login, Register and Logout flow; 52 | - Screens 53 | - Splash; 54 | - Register (account, establishment and professional); 55 | - Login; 56 | - Home (customized for each user); 57 | - Schedule; 58 | - Employee flow; 59 | - Calendar flow; 60 | - Packages: 61 | - [asyncstate](https://pub.dev/packages/asyncstate) 62 | - [dio](https://pub.dev/packages/dio) 63 | - [intl](https://pub.dev/packages/intl) 64 | - [loading_animation_widget](https://pub.dev/packages/loading_animation_widget) 65 | - [pretty_dio_logger](https://pub.dev/packages/pretty_dio_logger) 66 | - [shared_preferences](https://pub.dev/packages/shared_preferences) 67 | - [syncfusion_flutter_calendar](https://pub.dev/packages/syncfusion_flutter_calendar) 68 | - [table_calendar](https://pub.dev/packages/table_calendar) 69 | - [top_snackbar_flutter](https://pub.dev/packages/top_snackbar_flutter) 70 | - [validatorless](https://pub.dev/packages/validatorless) 71 | - [build_runner](https://pub.dev/packages/build_runner) 72 | - [custom_lint](https://pub.dev/packages/custom_lint) 73 | - [flutter_lints](https://pub.dev/packages/flutter_lints) 74 | - [riverpod_annotation](https://pub.dev/packages/riverpod_annotation), [riverpod_generator](https://pub.dev/packages/riverpod_generator), [riverpod_lint](https://pub.dev/packages/riverpod_lint) 75 | - Dentre outras coisas. 🔥 76 | 77 | --- 78 | 79 |

🤯 Challenges and Learning along the way

80 | 81 | Without a doubt, the biggest learning experience was Riverpod; I realized that it is very interesting for managing the state of an application, going further in points that can be crucial (such as being able to have multiple providers of the same type, performing dispose when it is no longer used, among other things). 82 | 83 | Furthermore, it was really cool to learn more about the powers and uses of Dart 3 in practice, such as the new `switch` and the use of `pattern matching`; which fit very well and make the code more objective. Another point that was very interesting was the `Either` used, similar to what is [in this article](https://codewithandrea.com/articles/flutter-exception-handling-try-catch-result-type/); without any external dependency. You can use functional programming with `pattern matching` to handle error and success cases. 84 | 85 | Various other learnings were acquired and also reinforced; in addition to refactorings and code improvements made along the way. There is always something to be improved and learned. 🚀 86 | 87 | --- 88 | 89 |

🤔 How to use

90 | 91 | ``` 92 | Configure the Flutter environment on your machine: 93 | https://flutter.dev/docs/get-started/install 94 | 95 | - Clone the repository: 96 | $ git clone https://github.com/felipecastrosales/barbershop 97 | 98 | - Enter the directory: 99 | $ cd barbershop 100 | 101 | - Open the project: 102 | $ code . 103 | 104 | - Install the json_rest_server: 105 | $ dart pub global activate json_rest_server 106 | 107 | - Enter the api directory: 108 | $ cd api 109 | 110 | - Run the server: 111 | $ json_rest_server run 112 | 113 | - Enter the project directory: 114 | $ flutter pub get 115 | 116 | - Get your IP and put on `rest_client.dart`; 117 | 118 | - Execute the application: 119 | $ flutter run 120 | ``` 121 | 122 | --- 123 | 124 |

💪 How to contribute

125 | 126 | There are several ways to contribute to this project, such as: 127 | 128 | ``` 129 | - Fork this repository; 130 | 131 | - Create a branch with your feature: 132 | $ git checkout -b my-feature 133 | 134 | - Commit your changes: 135 | $ git commit -m "feature: My new feature" 136 | 137 | - Push your branch: 138 | $ git push origin my-feature 139 | 140 | - Open a pull request on this repository and/or create an issue explaining your problem. 141 | ``` 142 | 143 | --- 144 | 145 |

📝 License

146 | 147 | This repository is under the MIT License, and you can see it in the LICENSE file for more details. 148 | 149 | --- 150 | 151 | > This project was developed with ❤️ by **[@Felipe Sales](https://www.linkedin.com/in/felipecastrosales/)**, with the instructor **[@Rodrigo Rahman](https://linkedin.com/in/rodrigo-rahman)**. 152 | If this helped you, give it a ⭐, and contribute, it will help me too. 😉 153 | 154 | --- 155 | 156 |
157 | 158 | [![Linkedin Badge](https://img.shields.io/badge/-Felipe%20Sales-292929?style=flat-square&logo=Linkedin&logoColor=white&link=https://www.linkedin.com/in/felipecastrosales/)](https://www.linkedin.com/in/felipecastrosales/) 159 | 160 |
161 | -------------------------------------------------------------------------------- /lib/src/features/auth/login/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/constants/constants.dart'; 2 | import 'package:barbershop/src/core/ui/helpers/form_helper.dart'; 3 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 4 | import 'package:barbershop/src/features/auth/login/login_state.dart'; 5 | import 'package:barbershop/src/features/auth/login/login_vm.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | import 'package:validatorless/validatorless.dart'; 9 | 10 | final class LoginPage extends ConsumerStatefulWidget { 11 | const LoginPage({super.key}); 12 | 13 | @override 14 | ConsumerState createState() => _LoginPageState(); 15 | } 16 | 17 | final class _LoginPageState extends ConsumerState { 18 | final formKey = GlobalKey(); 19 | final emailController = TextEditingController(); 20 | final passwordController = TextEditingController(); 21 | 22 | @override 23 | void dispose() { 24 | emailController.dispose(); 25 | passwordController.dispose(); 26 | super.dispose(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | final LoginVM(:login) = ref.watch(loginVMProvider.notifier); 32 | 33 | ref.listen( 34 | loginVMProvider, 35 | (_, state) { 36 | debugPrint('state: ${state.status}'); 37 | switch (state) { 38 | case LoginState(status: LoginStateStatus.initial): 39 | break; 40 | case LoginState(status: LoginStateStatus.error, :final errorMessage?): 41 | context.showError(errorMessage); 42 | case LoginState(status: LoginStateStatus.error): 43 | context.showError('Erro ao realizar login'); 44 | case LoginState(status: LoginStateStatus.admLogin): 45 | Navigator.of(context) 46 | .pushNamedAndRemoveUntil('/home/adm', (_) => false); 47 | case LoginState(status: LoginStateStatus.employeeLogin): 48 | Navigator.of(context) 49 | .pushNamedAndRemoveUntil('/home/employee', (_) => false); 50 | } 51 | }, 52 | ); 53 | 54 | return Scaffold( 55 | backgroundColor: Colors.black, 56 | resizeToAvoidBottomInset: false, 57 | body: DecoratedBox( 58 | decoration: const BoxDecoration( 59 | image: DecorationImage( 60 | image: AssetImage(AppImages.backgroundChair), 61 | fit: BoxFit.cover, 62 | opacity: 0.2, 63 | ), 64 | ), 65 | child: Padding( 66 | padding: const EdgeInsets.all(30), 67 | child: Form( 68 | key: formKey, 69 | child: CustomScrollView( 70 | slivers: [ 71 | SliverFillRemaining( 72 | hasScrollBody: false, 73 | child: Stack( 74 | alignment: Alignment.center, 75 | children: [ 76 | Column( 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | children: [ 79 | Image.asset( 80 | AppImages.imgLogo, 81 | width: 150, 82 | height: 170, 83 | ), 84 | const SizedBox(height: 24), 85 | TextFormField( 86 | controller: emailController, 87 | onTapOutside: (_) => context.unfocus(), 88 | keyboardType: TextInputType.emailAddress, 89 | validator: Validatorless.multiple([ 90 | Validatorless.required('E-mail obrigatório'), 91 | Validatorless.email('E-mail inválido'), 92 | ]), 93 | decoration: const InputDecoration( 94 | label: Text('E-mail'), 95 | labelStyle: TextStyle(color: Colors.black), 96 | hintText: 'E-mail', 97 | hintStyle: TextStyle(color: Colors.black), 98 | floatingLabelBehavior: 99 | FloatingLabelBehavior.never, 100 | ), 101 | ), 102 | const SizedBox(height: 24), 103 | TextFormField( 104 | controller: passwordController, 105 | onTapOutside: (_) => context.unfocus(), 106 | validator: Validatorless.multiple([ 107 | Validatorless.required('Senha obrigatória'), 108 | Validatorless.min(6, 'Senha inválida'), 109 | ]), 110 | obscureText: true, 111 | decoration: const InputDecoration( 112 | label: Text('Senha'), 113 | labelStyle: TextStyle(color: Colors.black), 114 | hintText: 'Senha', 115 | hintStyle: TextStyle(color: Colors.black), 116 | floatingLabelBehavior: 117 | FloatingLabelBehavior.never, 118 | ), 119 | ), 120 | const SizedBox(height: 16), 121 | const Align( 122 | alignment: Alignment.centerLeft, 123 | child: Text( 124 | 'Esqueceu a senha?', 125 | style: TextStyle( 126 | color: AppColors.brown, 127 | fontSize: 12, 128 | ), 129 | ), 130 | ), 131 | const SizedBox(height: 24), 132 | ElevatedButton( 133 | onPressed: () => 134 | switch (formKey.currentState?.validate()) { 135 | (false || null) => 136 | context.showError('Campos inválidos'), 137 | true => login( 138 | emailController.text, 139 | passwordController.text, 140 | ), 141 | }, 142 | style: ElevatedButton.styleFrom( 143 | minimumSize: const Size.fromHeight(56), 144 | ), 145 | child: const Text('ACESSAR'), 146 | ), 147 | ], 148 | ), 149 | Align( 150 | alignment: Alignment.bottomCenter, 151 | child: InkWell( 152 | onTap: () => Navigator.of(context).pushNamed( 153 | '/auth/register/user', 154 | ), 155 | child: const Text( 156 | 'Criar conta', 157 | style: TextStyle( 158 | color: Colors.white, 159 | fontSize: 16, 160 | fontWeight: FontWeight.w500, 161 | ), 162 | ), 163 | ), 164 | ), 165 | ], 166 | ), 167 | ), 168 | ], 169 | ), 170 | ), 171 | ), 172 | ), 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/src/features/schedule/schedule_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:barbershop/src/core/ui/helpers/form_helper.dart'; 2 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 3 | import 'package:barbershop/src/core/ui/widgets/avatar_widget.dart'; 4 | import 'package:barbershop/src/core/ui/widgets/hours_panel.dart'; 5 | import 'package:barbershop/src/features/schedule/schedule_state.dart'; 6 | import 'package:barbershop/src/features/schedule/schedule_vm.dart'; 7 | import 'package:barbershop/src/features/schedule/widgets/schedule_calendar.dart'; 8 | import 'package:barbershop/src/models/user_model.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 11 | import 'package:intl/intl.dart'; 12 | import 'package:validatorless/validatorless.dart'; 13 | 14 | class SchedulePage extends ConsumerStatefulWidget { 15 | const SchedulePage({super.key}); 16 | 17 | @override 18 | ConsumerState createState() => _SchedulePageState(); 19 | } 20 | 21 | class _SchedulePageState extends ConsumerState { 22 | var dateFormat = DateFormat('dd/MM/yyyy'); 23 | var showCalendar = false; 24 | 25 | final formKey = GlobalKey(); 26 | final clientController = TextEditingController(); 27 | final dateController = TextEditingController(); 28 | 29 | @override 30 | void dispose() { 31 | clientController.dispose(); 32 | dateController.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | final userModel = ModalRoute.of(context)?.settings.arguments as UserModel; 39 | final scheduleVM = ref.watch(scheduleVMProvider.notifier); 40 | 41 | final employeeData = switch (userModel) { 42 | UserModelADM(:final workDays, :final workHours) => ( 43 | workDays: workDays!, 44 | workHours: workHours!, 45 | ), 46 | UserModelEmployee(:final workDays, :final workHours) => ( 47 | workDays: workDays, 48 | workHours: workHours, 49 | ), 50 | }; 51 | 52 | ref.listen( 53 | scheduleVMProvider.select((state) => state.status), 54 | (_, status) { 55 | switch (status) { 56 | case ScheduleStateStatus.initial: 57 | break; 58 | case ScheduleStateStatus.success: 59 | context.showSuccess('Cliente agendado com sucesso'); 60 | Navigator.of(context).pop(); 61 | case ScheduleStateStatus.error: 62 | context.showError('Erro ao registrar agendamento'); 63 | } 64 | }, 65 | ); 66 | 67 | return Scaffold( 68 | appBar: AppBar( 69 | title: const Text('Agendar Cliente'), 70 | ), 71 | body: SingleChildScrollView( 72 | child: Padding( 73 | padding: const EdgeInsets.all(24), 74 | child: Form( 75 | key: formKey, 76 | child: Center( 77 | child: Column( 78 | children: [ 79 | const AvatarWidget.withoutButton(), 80 | const SizedBox(height: 32), 81 | Text( 82 | userModel.name, 83 | style: const TextStyle( 84 | fontSize: 20, 85 | fontWeight: FontWeight.w500, 86 | ), 87 | ), 88 | const SizedBox(height: 40), 89 | TextFormField( 90 | controller: clientController, 91 | validator: Validatorless.required('Nome é obrigatório'), 92 | onTapOutside: (_) => context.unfocus(), 93 | decoration: const InputDecoration( 94 | border: OutlineInputBorder(), 95 | labelText: 'Nome do Cliente', 96 | ), 97 | ), 98 | const SizedBox(height: 10), 99 | TextFormField( 100 | controller: dateController, 101 | validator: Validatorless.required('A data é obrigatória'), 102 | readOnly: true, 103 | onTap: () { 104 | setState(() { 105 | showCalendar = true; 106 | }); 107 | context.unfocus(); 108 | }, 109 | decoration: const InputDecoration( 110 | border: OutlineInputBorder(), 111 | labelText: 'Selecione uma data', 112 | hintText: 'Selecione uma data', 113 | floatingLabelBehavior: FloatingLabelBehavior.never, 114 | suffixIcon: Icon( 115 | Icons.calendar_today, 116 | color: Colors.grey, 117 | size: 20, 118 | ), 119 | ), 120 | ), 121 | const SizedBox(height: 10), 122 | Offstage( 123 | offstage: !showCalendar, 124 | child: Column( 125 | children: [ 126 | const SizedBox(height: 24), 127 | ScheduleCalendar( 128 | workDays: employeeData.workDays, 129 | cancelPressed: () { 130 | setState(() { 131 | showCalendar = false; 132 | }); 133 | }, 134 | onOkPressed: (DateTime value) { 135 | setState(() { 136 | dateController.text = dateFormat.format(value); 137 | scheduleVM.dateSelect(value); 138 | showCalendar = false; 139 | }); 140 | }, 141 | ), 142 | ], 143 | ), 144 | ), 145 | const SizedBox(height: 24), 146 | HoursPanel.singleSelection( 147 | startTime: 6, 148 | endTime: 23, 149 | onTimePressed: scheduleVM.timeSelect, 150 | enabledTimes: employeeData.workHours, 151 | ), 152 | const SizedBox(height: 24), 153 | ElevatedButton( 154 | style: ElevatedButton.styleFrom( 155 | minimumSize: const Size.fromHeight(56), 156 | ), 157 | onPressed: () { 158 | switch (formKey.currentState?.validate()) { 159 | case null || false: 160 | context.showError('Dados incompletos'); 161 | case true: 162 | final hasHourSelected = ref.watch( 163 | scheduleVMProvider 164 | .select((state) => state.scheduleTime != null), 165 | ); 166 | if (hasHourSelected) { 167 | scheduleVM.register( 168 | user: userModel, 169 | clientName: clientController.text, 170 | ); 171 | } else { 172 | context.showError( 173 | 'Selecione um horário de atendimento', 174 | ); 175 | } 176 | } 177 | }, 178 | child: const Text('AGENDAR'), 179 | ), 180 | const SizedBox(height: 24), 181 | ], 182 | ), 183 | ), 184 | ), 185 | ), 186 | ), 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /lib/src/features/employee/register/employee_register_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:barbershop/src/core/providers/application_providers.dart'; 4 | import 'package:barbershop/src/core/ui/helpers/form_helper.dart'; 5 | import 'package:barbershop/src/core/ui/helpers/messages.dart'; 6 | import 'package:barbershop/src/core/ui/widgets/avatar_widget.dart'; 7 | import 'package:barbershop/src/core/ui/widgets/barbershop_loader.dart'; 8 | import 'package:barbershop/src/core/ui/widgets/hours_panel.dart'; 9 | import 'package:barbershop/src/core/ui/widgets/weekdays_panel.dart'; 10 | import 'package:barbershop/src/features/employee/register/employee_register_state.dart'; 11 | import 'package:barbershop/src/features/employee/register/employee_register_vm.dart'; 12 | import 'package:barbershop/src/models/barbershop_model.dart'; 13 | import 'package:flutter/material.dart'; 14 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 15 | import 'package:validatorless/validatorless.dart'; 16 | 17 | class EmployeeRegisterPage extends ConsumerStatefulWidget { 18 | const EmployeeRegisterPage({super.key}); 19 | 20 | @override 21 | ConsumerState createState() => 22 | _EmployeeRegisterPageState(); 23 | } 24 | 25 | class _EmployeeRegisterPageState extends ConsumerState { 26 | var registerADM = false; 27 | final formKey = GlobalKey(); 28 | final nameController = TextEditingController(); 29 | final emailController = TextEditingController(); 30 | final passwordController = TextEditingController(); 31 | 32 | @override 33 | void dispose() { 34 | super.dispose(); 35 | nameController.dispose(); 36 | emailController.dispose(); 37 | passwordController.dispose(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | final employeeRegisterVM = ref.watch(employeeRegisterVMProvider.notifier); 43 | final barbershopAsyncValue = ref.watch(getMyBarbershopProvider); 44 | 45 | ref.listen( 46 | employeeRegisterVMProvider.select((state) => state.status), 47 | (_, status) { 48 | switch (status) { 49 | case EmployeeRegisterStateStatus.initial: 50 | break; 51 | case EmployeeRegisterStateStatus.success: 52 | context.showSuccess('Colaborador registrado com sucesso'); 53 | Navigator.of(context).pop(); 54 | case EmployeeRegisterStateStatus.error: 55 | context.showError('Erro ao registar colaborador'); 56 | } 57 | }, 58 | ); 59 | 60 | return Scaffold( 61 | appBar: AppBar( 62 | title: const Text('Cadastrar Colaborador'), 63 | ), 64 | body: barbershopAsyncValue.when( 65 | loading: () => const BarbershopLoader(), 66 | error: (e, s) { 67 | log('Erro ao carregar a página', error: e, stackTrace: s); 68 | return const Center( 69 | child: Text('Erro ao carregar a página'), 70 | ); 71 | }, 72 | data: (barbershop) { 73 | final BarbershopModel(:openDays, :openHours) = barbershop; 74 | 75 | return SingleChildScrollView( 76 | child: Padding( 77 | padding: const EdgeInsets.all(12), 78 | child: Form( 79 | key: formKey, 80 | child: Center( 81 | child: Column( 82 | children: [ 83 | const AvatarWidget(), 84 | const SizedBox(height: 32), 85 | Row( 86 | children: [ 87 | Checkbox.adaptive( 88 | value: registerADM, 89 | onChanged: (_) { 90 | setState(() { 91 | registerADM = !registerADM; 92 | }); 93 | employeeRegisterVM.setRegisterADM(registerADM); 94 | }, 95 | ), 96 | const Expanded( 97 | child: Text( 98 | 'Sou administrador e quero me cadastrar como colaborador', 99 | style: TextStyle(fontSize: 14), 100 | ), 101 | ), 102 | ], 103 | ), 104 | Offstage( 105 | offstage: registerADM, 106 | child: Column( 107 | children: [ 108 | const SizedBox(height: 24), 109 | TextFormField( 110 | onTapOutside: (_) => context.unfocus(), 111 | controller: nameController, 112 | decoration: const InputDecoration( 113 | label: Text('Nome'), 114 | ), 115 | validator: registerADM 116 | ? null 117 | : Validatorless.required( 118 | 'Nome é obrigatório', 119 | ), 120 | ), 121 | const SizedBox(height: 24), 122 | TextFormField( 123 | onTapOutside: (_) => context.unfocus(), 124 | controller: emailController, 125 | keyboardType: TextInputType.emailAddress, 126 | decoration: const InputDecoration( 127 | label: Text('Email'), 128 | ), 129 | validator: registerADM 130 | ? null 131 | : Validatorless.multiple([ 132 | Validatorless.required( 133 | 'E-mail é obrigatório', 134 | ), 135 | Validatorless.email( 136 | 'Digite um e-mail válido', 137 | ), 138 | ]), 139 | ), 140 | const SizedBox(height: 24), 141 | TextFormField( 142 | onTapOutside: (_) => context.unfocus(), 143 | obscureText: true, 144 | controller: passwordController, 145 | decoration: const InputDecoration( 146 | label: Text('Senha'), 147 | ), 148 | validator: registerADM 149 | ? null 150 | : Validatorless.multiple([ 151 | Validatorless.required( 152 | 'Senha é obrigatória', 153 | ), 154 | Validatorless.min( 155 | 6, 156 | 'Senha deve conter no mínimo 6 caracteres', 157 | ), 158 | ]), 159 | ), 160 | ], 161 | ), 162 | ), 163 | const SizedBox(height: 24), 164 | WeekdaysPanel( 165 | enabledDays: openDays, 166 | onDayPressed: employeeRegisterVM.addOrRemoveWeekDays, 167 | ), 168 | const SizedBox(height: 24), 169 | HoursPanel( 170 | startTime: 6, 171 | endTime: 23, 172 | enabledTimes: openHours, 173 | onTimePressed: employeeRegisterVM.addOrRemoveWorkHours, 174 | ), 175 | const SizedBox(height: 24), 176 | ElevatedButton( 177 | style: ElevatedButton.styleFrom( 178 | minimumSize: const Size.fromHeight(56), 179 | ), 180 | onPressed: () { 181 | switch (formKey.currentState?.validate()) { 182 | case false || null: 183 | context.showError('Existem campos inválidos'); 184 | case true: 185 | final EmployeeRegisterState( 186 | workDays: List(isEmpty: hasWorkDays), 187 | workHours: List(isEmpty: hasWorkHours), 188 | ) = ref.watch(employeeRegisterVMProvider); 189 | 190 | if (hasWorkDays || hasWorkHours) { 191 | context.showError( 192 | 'Selecione os dias da semana e horário de atendimento', 193 | ); 194 | return; 195 | } 196 | 197 | employeeRegisterVM.register( 198 | name: nameController.text, 199 | email: emailController.text, 200 | password: passwordController.text, 201 | ); 202 | } 203 | }, 204 | child: const Text('CADASTRAR COLABORADOR'), 205 | ), 206 | ], 207 | ), 208 | ), 209 | ), 210 | ), 211 | ); 212 | }, 213 | ), 214 | ); 215 | } 216 | } 217 | --------------------------------------------------------------------------------