diff --git a/lib/application/auth/logout/logout_bloc.dart b/lib/application/auth/logout/logout_bloc.dart new file mode 100644 index 0000000..df01479 --- /dev/null +++ b/lib/application/auth/logout/logout_bloc.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/auth/auth.dart'; + +part 'logout_event.dart'; +part 'logout_state.dart'; +part 'logout_bloc.freezed.dart'; + +@injectable +class LogoutBloc extends Bloc { + final IAuthRepository _authRepository; + LogoutBloc(this._authRepository) : super(LogoutState.initial()) { + on(_onLogoutEvent); + } + + Future _onLogoutEvent(LogoutEvent event, Emitter emit) { + return event.map( + logout: (e) async { + Either? failureOrSuccess; + emit( + state.copyWith(isLoggingOut: true, logoutFailureOrSuccess: none()), + ); + + failureOrSuccess = await _authRepository.logout(); + + emit( + state.copyWith( + isLoggingOut: false, + logoutFailureOrSuccess: optionOf(failureOrSuccess), + ), + ); + }, + ); + } +} diff --git a/lib/application/auth/logout/logout_bloc.freezed.dart b/lib/application/auth/logout/logout_bloc.freezed.dart new file mode 100644 index 0000000..be5dcfb --- /dev/null +++ b/lib/application/auth/logout/logout_bloc.freezed.dart @@ -0,0 +1,332 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'logout_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$LogoutEvent { + @optionalTypeArgs + TResult when({required TResult Function() logout}) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({TResult? Function()? logout}) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? logout, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Logout value) logout, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Logout value)? logout, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Logout value)? logout, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LogoutEventCopyWith<$Res> { + factory $LogoutEventCopyWith( + LogoutEvent value, + $Res Function(LogoutEvent) then, + ) = _$LogoutEventCopyWithImpl<$Res, LogoutEvent>; +} + +/// @nodoc +class _$LogoutEventCopyWithImpl<$Res, $Val extends LogoutEvent> + implements $LogoutEventCopyWith<$Res> { + _$LogoutEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LogoutEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$LogoutImplCopyWith<$Res> { + factory _$$LogoutImplCopyWith( + _$LogoutImpl value, + $Res Function(_$LogoutImpl) then, + ) = __$$LogoutImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LogoutImplCopyWithImpl<$Res> + extends _$LogoutEventCopyWithImpl<$Res, _$LogoutImpl> + implements _$$LogoutImplCopyWith<$Res> { + __$$LogoutImplCopyWithImpl( + _$LogoutImpl _value, + $Res Function(_$LogoutImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LogoutEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LogoutImpl implements _Logout { + const _$LogoutImpl(); + + @override + String toString() { + return 'LogoutEvent.logout()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LogoutImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({required TResult Function() logout}) { + return logout(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({TResult? Function()? logout}) { + return logout?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? logout, + required TResult orElse(), + }) { + if (logout != null) { + return logout(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Logout value) logout, + }) { + return logout(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Logout value)? logout, + }) { + return logout?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Logout value)? logout, + required TResult orElse(), + }) { + if (logout != null) { + return logout(this); + } + return orElse(); + } +} + +abstract class _Logout implements LogoutEvent { + const factory _Logout() = _$LogoutImpl; +} + +/// @nodoc +mixin _$LogoutState { + Option> get logoutFailureOrSuccess => + throw _privateConstructorUsedError; + bool get isLoggingOut => throw _privateConstructorUsedError; + + /// Create a copy of LogoutState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LogoutStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LogoutStateCopyWith<$Res> { + factory $LogoutStateCopyWith( + LogoutState value, + $Res Function(LogoutState) then, + ) = _$LogoutStateCopyWithImpl<$Res, LogoutState>; + @useResult + $Res call({ + Option> logoutFailureOrSuccess, + bool isLoggingOut, + }); +} + +/// @nodoc +class _$LogoutStateCopyWithImpl<$Res, $Val extends LogoutState> + implements $LogoutStateCopyWith<$Res> { + _$LogoutStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LogoutState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? logoutFailureOrSuccess = null, + Object? isLoggingOut = null, + }) { + return _then( + _value.copyWith( + logoutFailureOrSuccess: null == logoutFailureOrSuccess + ? _value.logoutFailureOrSuccess + : logoutFailureOrSuccess // ignore: cast_nullable_to_non_nullable + as Option>, + isLoggingOut: null == isLoggingOut + ? _value.isLoggingOut + : isLoggingOut // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$LogoutStateImplCopyWith<$Res> + implements $LogoutStateCopyWith<$Res> { + factory _$$LogoutStateImplCopyWith( + _$LogoutStateImpl value, + $Res Function(_$LogoutStateImpl) then, + ) = __$$LogoutStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + Option> logoutFailureOrSuccess, + bool isLoggingOut, + }); +} + +/// @nodoc +class __$$LogoutStateImplCopyWithImpl<$Res> + extends _$LogoutStateCopyWithImpl<$Res, _$LogoutStateImpl> + implements _$$LogoutStateImplCopyWith<$Res> { + __$$LogoutStateImplCopyWithImpl( + _$LogoutStateImpl _value, + $Res Function(_$LogoutStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LogoutState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? logoutFailureOrSuccess = null, + Object? isLoggingOut = null, + }) { + return _then( + _$LogoutStateImpl( + logoutFailureOrSuccess: null == logoutFailureOrSuccess + ? _value.logoutFailureOrSuccess + : logoutFailureOrSuccess // ignore: cast_nullable_to_non_nullable + as Option>, + isLoggingOut: null == isLoggingOut + ? _value.isLoggingOut + : isLoggingOut // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$LogoutStateImpl implements _LogoutState { + _$LogoutStateImpl({ + required this.logoutFailureOrSuccess, + this.isLoggingOut = false, + }); + + @override + final Option> logoutFailureOrSuccess; + @override + @JsonKey() + final bool isLoggingOut; + + @override + String toString() { + return 'LogoutState(logoutFailureOrSuccess: $logoutFailureOrSuccess, isLoggingOut: $isLoggingOut)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LogoutStateImpl && + (identical(other.logoutFailureOrSuccess, logoutFailureOrSuccess) || + other.logoutFailureOrSuccess == logoutFailureOrSuccess) && + (identical(other.isLoggingOut, isLoggingOut) || + other.isLoggingOut == isLoggingOut)); + } + + @override + int get hashCode => + Object.hash(runtimeType, logoutFailureOrSuccess, isLoggingOut); + + /// Create a copy of LogoutState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LogoutStateImplCopyWith<_$LogoutStateImpl> get copyWith => + __$$LogoutStateImplCopyWithImpl<_$LogoutStateImpl>(this, _$identity); +} + +abstract class _LogoutState implements LogoutState { + factory _LogoutState({ + required final Option> logoutFailureOrSuccess, + final bool isLoggingOut, + }) = _$LogoutStateImpl; + + @override + Option> get logoutFailureOrSuccess; + @override + bool get isLoggingOut; + + /// Create a copy of LogoutState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LogoutStateImplCopyWith<_$LogoutStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/application/auth/logout/logout_event.dart b/lib/application/auth/logout/logout_event.dart new file mode 100644 index 0000000..265d553 --- /dev/null +++ b/lib/application/auth/logout/logout_event.dart @@ -0,0 +1,6 @@ +part of 'logout_bloc.dart'; + +@freezed +class LogoutEvent with _$LogoutEvent { + const factory LogoutEvent.logout() = _Logout; +} diff --git a/lib/application/auth/logout/logout_state.dart b/lib/application/auth/logout/logout_state.dart new file mode 100644 index 0000000..a1d5ade --- /dev/null +++ b/lib/application/auth/logout/logout_state.dart @@ -0,0 +1,11 @@ +part of 'logout_bloc.dart'; + +@freezed +class LogoutState with _$LogoutState { + factory LogoutState({ + required Option> logoutFailureOrSuccess, + @Default(false) bool isLoggingOut, + }) = _LogoutState; + + factory LogoutState.initial() => LogoutState(logoutFailureOrSuccess: none()); +} diff --git a/lib/common/url/api_path.dart b/lib/common/url/api_path.dart index 216dbea..f7c5c50 100644 --- a/lib/common/url/api_path.dart +++ b/lib/common/url/api_path.dart @@ -1,5 +1,6 @@ class ApiPath { static const String login = '/api/v1/auth/login'; + static const String logout = '/api/v1/auth/logout'; static const String outlets = '/api/v1/outlets'; static const String categories = '/api/v1/categories'; static const String products = '/api/v1/products'; diff --git a/lib/domain/auth/repositories/i_auth_repository.dart b/lib/domain/auth/repositories/i_auth_repository.dart index ebeeece..569853f 100644 --- a/lib/domain/auth/repositories/i_auth_repository.dart +++ b/lib/domain/auth/repositories/i_auth_repository.dart @@ -5,6 +5,7 @@ abstract class IAuthRepository { required String email, required String password, }); + Future> logout(); Future hasToken(); Future> currentUser(); } diff --git a/lib/infrastructure/auth/datasources/remote_data_provider.dart b/lib/infrastructure/auth/datasources/remote_data_provider.dart index 7f787eb..3508b78 100644 --- a/lib/infrastructure/auth/datasources/remote_data_provider.dart +++ b/lib/infrastructure/auth/datasources/remote_data_provider.dart @@ -1,10 +1,12 @@ import 'dart:developer'; +import 'package:dartz/dartz.dart'; import 'package:data_channel/data_channel.dart'; import 'package:injectable/injectable.dart'; import '../../../common/api/api_client.dart'; import '../../../common/api/api_failure.dart'; +import '../../../common/function/app_function.dart'; import '../../../common/url/api_path.dart'; import '../../../domain/auth/auth.dart'; import '../auth_dtos.dart'; @@ -41,4 +43,22 @@ class AuthRemoteDataProvider { return DC.error(AuthFailure.serverError(e)); } } + + Future> logout() async { + try { + final response = await _apiClient.post( + ApiPath.logout, + headers: getAuthorizationHeader(), + ); + + if (response.data['success'] == false) { + return DC.error(AuthFailure.unexpectedError()); + } + + return DC.data(unit); + } on ApiFailure catch (e, s) { + log('logout', name: _logName, error: e, stackTrace: s); + return DC.error(AuthFailure.serverError(e)); + } + } } diff --git a/lib/infrastructure/auth/repositories/auth_repository.dart b/lib/infrastructure/auth/repositories/auth_repository.dart index 93a21b7..823145c 100644 --- a/lib/infrastructure/auth/repositories/auth_repository.dart +++ b/lib/infrastructure/auth/repositories/auth_repository.dart @@ -57,4 +57,21 @@ class AuthRepository implements IAuthRepository { Future hasToken() async { return await _localDataProvider.hasToken(); } + + @override + Future> logout() async { + try { + final result = await _dataProvider.logout(); + + if (result.hasError) { + return left(result.error!); + } + + await _localDataProvider.deleteAllAuth(); + return right(unit); + } catch (e, s) { + log('logoutError', name: _logName, error: e, stackTrace: s); + return left(const AuthFailure.unexpectedError()); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index a9899fc..2ca4dc4 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -26,6 +26,8 @@ import 'package:apskel_pos_flutter_v2/application/analytic/sales_analytic_loader import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343; import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart' as _i46; +import 'package:apskel_pos_flutter_v2/application/auth/logout/logout_bloc.dart' + as _i641; import 'package:apskel_pos_flutter_v2/application/category/category_loader/category_loader_bloc.dart' as _i1018; import 'package:apskel_pos_flutter_v2/application/checkout/checkout_form/checkout_form_bloc.dart' @@ -293,6 +295,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i46.LoginFormBloc>( () => _i46.LoginFormBloc(gh<_i776.IAuthRepository>()), ); + gh.factory<_i641.LogoutBloc>( + () => _i641.LogoutBloc(gh<_i776.IAuthRepository>()), + ); gh.factory<_i1018.CategoryLoaderBloc>( () => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()), ); diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index bf43d8c..90e58f3 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/auth/auth_bloc.dart'; +import '../application/auth/logout/logout_bloc.dart'; import '../application/category/category_loader/category_loader_bloc.dart'; import '../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../application/customer/customer_loader/customer_loader_bloc.dart'; @@ -57,6 +58,7 @@ class _AppWidgetState extends State { BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/components/dialog/logout_modal_dialog.dart b/lib/presentation/components/dialog/logout_modal_dialog.dart new file mode 100644 index 0000000..95a0c0c --- /dev/null +++ b/lib/presentation/components/dialog/logout_modal_dialog.dart @@ -0,0 +1,57 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../application/auth/logout/logout_bloc.dart'; +import '../../../common/theme/theme.dart'; +import '../button/button.dart'; +import '../spaces/space.dart'; +import 'dialog.dart'; + +class LogoutModalDialog extends StatelessWidget { + const LogoutModalDialog({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return CustomModalDialog( + title: 'Konfirmasi', + contentPadding: EdgeInsets.all(16), + bottom: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: AppElevatedButton.outlined( + onPressed: () => context.maybePop(), + label: 'Batal', + ), + ), + SpaceWidth(12), + Expanded( + child: AppElevatedButton.filled( + onPressed: state.isLoggingOut + ? null + : () { + context.read().add( + const LogoutEvent.logout(), + ); + context.maybePop(); + }, + label: 'Ya', + isLoading: state.isLoggingOut, + ), + ), + ], + ), + ), + child: Text( + 'Apakah anda yakin ingin keluar?', + style: AppStyle.lg.copyWith(fontWeight: FontWeight.w600), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/pages/main/main_page.dart b/lib/presentation/pages/main/main_page.dart index 4017640..6d15d68 100644 --- a/lib/presentation/pages/main/main_page.dart +++ b/lib/presentation/pages/main/main_page.dart @@ -1,8 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/auth/logout/logout_bloc.dart'; import '../../../common/theme/theme.dart'; import '../../components/assets/assets.gen.dart'; +import '../../components/dialog/logout_modal_dialog.dart'; +import '../../components/toast/flushbar.dart'; import '../../router/app_router.gr.dart'; @RoutePage() @@ -11,107 +15,140 @@ class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { - return AutoTabsRouter( - routes: [ - HomeRoute(), - TableRoute(), - ReportRoute(), - CustomerRoute(), - SettingRoute(), - ], - builder: (context, child) { - final tabsRouter = AutoTabsRouter.of(context); - - return Scaffold( - body: Row( - children: [ - NavigationRail( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: tabsRouter.setActiveIndex, - labelType: NavigationRailLabelType.none, - backgroundColor: AppColor.primary, - selectedIconTheme: const IconThemeData(color: Colors.white), - indicatorColor: AppColor.disabled.withOpacity(0.25), - indicatorShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - minExtendedWidth: 56, - leading: Padding( - padding: EdgeInsets.all(8.0), - child: Assets.images.logoWhite.image( - width: 40, - height: 40, - fit: BoxFit.contain, - ), - ), - trailing: Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: IconButton( - icon: const Icon( - Icons.logout, - color: AppColor.disabled, - ), - onPressed: () {}, - tooltip: 'Logout', - ), - ), - ), - ), - destinations: const [ - NavigationRailDestination( - icon: Icon(Icons.home_outlined, color: AppColor.disabled), - selectedIcon: Icon(Icons.home, color: AppColor.white), - label: Text('POS'), - padding: EdgeInsets.symmetric(vertical: 8), - ), - NavigationRailDestination( - icon: Icon( - Icons.table_bar_outlined, - color: AppColor.disabled, - ), - selectedIcon: Icon(Icons.table_bar, color: AppColor.white), - label: Text('Meja'), - padding: EdgeInsets.symmetric(vertical: 8), - ), - NavigationRailDestination( - icon: Icon( - Icons.pie_chart_outline_outlined, - color: AppColor.disabled, - ), - selectedIcon: Icon(Icons.pie_chart, color: AppColor.white), - label: Text('Laporan'), - padding: EdgeInsets.symmetric(vertical: 8), - ), - NavigationRailDestination( - icon: Icon( - Icons.person_2_outlined, - color: AppColor.disabled, - ), - selectedIcon: Icon(Icons.person_2, color: AppColor.white), - label: Text('Pelanggan'), - padding: EdgeInsets.symmetric(vertical: 8), - ), - NavigationRailDestination( - icon: Icon( - Icons.settings_outlined, - color: AppColor.disabled, - ), - selectedIcon: Icon(Icons.settings, color: AppColor.white), - label: Text('Pengaturan'), - padding: EdgeInsets.symmetric(vertical: 8), - ), - ], - ), - const VerticalDivider(thickness: 1, width: 1), - // Main content area - Expanded(child: child), - ], + return BlocListener( + listenWhen: (p, c) => + p.logoutFailureOrSuccess != c.logoutFailureOrSuccess, + listener: (context, state) { + state.logoutFailureOrSuccess.fold( + () => null, + (either) => either.fold( + (f) { + AppFlushbar.showAuthFailureToast(context, f); + if (context.mounted) { + context.router.replaceAll([const LoginRoute()]); + } + }, + (_) { + if (context.mounted) { + context.router.replaceAll([const LoginRoute()]); + } + }, ), ); }, + child: AutoTabsRouter( + routes: [ + HomeRoute(), + TableRoute(), + ReportRoute(), + CustomerRoute(), + SettingRoute(), + ], + builder: (context, child) { + final tabsRouter = AutoTabsRouter.of(context); + + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: tabsRouter.activeIndex, + onDestinationSelected: tabsRouter.setActiveIndex, + labelType: NavigationRailLabelType.none, + backgroundColor: AppColor.primary, + selectedIconTheme: const IconThemeData(color: Colors.white), + indicatorColor: AppColor.disabled.withOpacity(0.25), + indicatorShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + minExtendedWidth: 56, + leading: Padding( + padding: EdgeInsets.all(8.0), + child: Assets.images.logoWhite.image( + width: 40, + height: 40, + fit: BoxFit.contain, + ), + ), + trailing: Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: IconButton( + icon: const Icon( + Icons.logout, + color: AppColor.disabled, + ), + onPressed: () { + showDialog( + context: context, + builder: (context) => LogoutModalDialog(), + ); + }, + tooltip: 'Logout', + ), + ), + ), + ), + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.home_outlined, color: AppColor.disabled), + selectedIcon: Icon(Icons.home, color: AppColor.white), + label: Text('POS'), + padding: EdgeInsets.symmetric(vertical: 8), + ), + NavigationRailDestination( + icon: Icon( + Icons.table_bar_outlined, + color: AppColor.disabled, + ), + selectedIcon: Icon( + Icons.table_bar, + color: AppColor.white, + ), + label: Text('Meja'), + padding: EdgeInsets.symmetric(vertical: 8), + ), + NavigationRailDestination( + icon: Icon( + Icons.pie_chart_outline_outlined, + color: AppColor.disabled, + ), + selectedIcon: Icon( + Icons.pie_chart, + color: AppColor.white, + ), + label: Text('Laporan'), + padding: EdgeInsets.symmetric(vertical: 8), + ), + NavigationRailDestination( + icon: Icon( + Icons.person_2_outlined, + color: AppColor.disabled, + ), + selectedIcon: Icon(Icons.person_2, color: AppColor.white), + label: Text('Pelanggan'), + padding: EdgeInsets.symmetric(vertical: 8), + ), + NavigationRailDestination( + icon: Icon( + Icons.settings_outlined, + color: AppColor.disabled, + ), + selectedIcon: Icon(Icons.settings, color: AppColor.white), + label: Text('Pengaturan'), + padding: EdgeInsets.symmetric(vertical: 8), + ), + ], + ), + const VerticalDivider(thickness: 1, width: 1), + // Main content area + Expanded(child: child), + ], + ), + ); + }, + ), ); } }