Compare commits

...

2 Commits

Author SHA1 Message Date
efrilm
5c683eda8d transfer table 2025-10-26 19:36:59 +07:00
efrilm
d5ed3f62ff create table 2025-10-26 18:47:51 +07:00
19 changed files with 1663 additions and 260 deletions

View File

@ -23,23 +23,74 @@ class TableFormBloc extends Bloc<TableFormEvent, TableFormState> {
Emitter<TableFormState> emit, Emitter<TableFormState> emit,
) { ) {
return event.map( return event.map(
updated: (e) async { nameChanged: (e) async {
emit(state.copyWith(name: e.name));
},
capacityChanged: (e) async {
emit(state.copyWith(capacity: int.tryParse(e.capacity) ?? 0));
},
created: (e) async {
Either<TableFailure, Table> failureOrTable; Either<TableFailure, Table> failureOrTable;
emit(state.copyWith(isUpdating: true, failureOrTable: none())); emit(state.copyWith(isCreating: true, failureOrTable: none()));
failureOrTable = await _repository.updatePosition( final isNameValid = state.name.isNotEmpty;
id: e.id, final isCapacityValid = state.capacity > 0;
position: e.position,
if (isNameValid && isCapacityValid) {
failureOrTable = await _repository.createTable(
name: state.name,
capacity: state.capacity,
); );
emit( emit(
state.copyWith( state.copyWith(
failureOrTable: optionOf(failureOrTable), failureOrTable: optionOf(failureOrTable),
isCreating: false,
),
);
}
emit(state.copyWith(failureOrTable: none(), isCreating: false));
},
updated: (e) async {
emit(state.copyWith(isUpdating: true, failureOrTable: none()));
await _repository.updatePosition(id: e.id, position: e.position);
emit(
state.copyWith(
// failureOrTable: optionOf(failureOrTable),
isUpdating: false, isUpdating: false,
), ),
); );
}, },
transfer: (e) async {
Either<TableFailure, Unit> failureOrTableTransfer;
emit(
state.copyWith(isTransfering: true, failureOrTableTransfer: none()),
);
final isFromTableIdValid = e.fromTableId.isNotEmpty;
final isToTableIdValid = e.toTableId.isNotEmpty;
if (isFromTableIdValid && isToTableIdValid) {
failureOrTableTransfer = await _repository.transferTable(
fromTableId: e.fromTableId,
toTableId: e.toTableId,
);
emit(
state.copyWith(
failureOrTableTransfer: optionOf(failureOrTableTransfer),
isTransfering: false,
),
);
}
emit(state.copyWith(failureOrTable: none(), isCreating: false));
},
); );
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,16 @@ part of 'table_form_bloc.dart';
@freezed @freezed
class TableFormEvent with _$TableFormEvent { class TableFormEvent with _$TableFormEvent {
const factory TableFormEvent.nameChanged(String name) = _NameChanged;
const factory TableFormEvent.capacityChanged(String capacity) =
_CapacityChanged;
const factory TableFormEvent.created() = _Created;
const factory TableFormEvent.updated({ const factory TableFormEvent.updated({
required String id, required String id,
required Offset position, required Offset position,
}) = _Updated; }) = _Updated;
const factory TableFormEvent.transfer({
required String fromTableId,
required String toTableId,
}) = _Transfer;
} }

View File

@ -3,9 +3,19 @@ part of 'table_form_bloc.dart';
@freezed @freezed
class TableFormState with _$TableFormState { class TableFormState with _$TableFormState {
factory TableFormState({ factory TableFormState({
required String name,
required int capacity,
required Option<Either<TableFailure, Table>> failureOrTable, required Option<Either<TableFailure, Table>> failureOrTable,
required Option<Either<TableFailure, Unit>> failureOrTableTransfer,
@Default(false) bool isUpdating, @Default(false) bool isUpdating,
@Default(false) bool isCreating,
@Default(false) bool isTransfering,
}) = _TableFormState; }) = _TableFormState;
factory TableFormState.initial() => TableFormState(failureOrTable: none()); factory TableFormState.initial() => TableFormState(
failureOrTable: none(),
name: '',
capacity: 0,
failureOrTableTransfer: none(),
);
} }

View File

@ -40,7 +40,11 @@ class TableLoaderBloc extends Bloc<TableLoaderEvent, TableLoaderState> {
emit(newState); emit(newState);
} }
newState = await _mapFetchedToState(newState, isRefresh: e.isRefresh); newState = await _mapFetchedToState(
newState,
isRefresh: e.isRefresh,
status: e.status,
);
emit(newState); emit(newState);
}, },
updatedPostion: (e) async { updatedPostion: (e) async {
@ -62,6 +66,7 @@ class TableLoaderBloc extends Bloc<TableLoaderEvent, TableLoaderState> {
Future<TableLoaderState> _mapFetchedToState( Future<TableLoaderState> _mapFetchedToState(
TableLoaderState state, { TableLoaderState state, {
bool isRefresh = false, bool isRefresh = false,
String? status,
}) async { }) async {
state = state.copyWith(isFetching: false); state = state.copyWith(isFetching: false);
@ -78,7 +83,10 @@ class TableLoaderBloc extends Bloc<TableLoaderEvent, TableLoaderState> {
); );
} }
final failureOrTable = await _repository.fetchTables(page: state.page); final failureOrTable = await _repository.fetchTables(
page: state.page,
status: status,
);
state = failureOrTable.fold( state = failureOrTable.fold(
(f) { (f) {

View File

@ -19,19 +19,19 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$TableLoaderEvent { mixin _$TableLoaderEvent {
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function(bool isRefresh) fetched, required TResult Function(bool isRefresh, String? status) fetched,
required TResult Function(String id, Offset position) updatedPostion, required TResult Function(String id, Offset position) updatedPostion,
required TResult Function(Table? table) setSelectedTable, required TResult Function(Table? table) setSelectedTable,
}) => throw _privateConstructorUsedError; }) => throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function(bool isRefresh)? fetched, TResult? Function(bool isRefresh, String? status)? fetched,
TResult? Function(String id, Offset position)? updatedPostion, TResult? Function(String id, Offset position)? updatedPostion,
TResult? Function(Table? table)? setSelectedTable, TResult? Function(Table? table)? setSelectedTable,
}) => throw _privateConstructorUsedError; }) => throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function(bool isRefresh)? fetched, TResult Function(bool isRefresh, String? status)? fetched,
TResult Function(String id, Offset position)? updatedPostion, TResult Function(String id, Offset position)? updatedPostion,
TResult Function(Table? table)? setSelectedTable, TResult Function(Table? table)? setSelectedTable,
required TResult orElse(), required TResult orElse(),
@ -86,7 +86,7 @@ abstract class _$$FetchedImplCopyWith<$Res> {
$Res Function(_$FetchedImpl) then, $Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>; ) = __$$FetchedImplCopyWithImpl<$Res>;
@useResult @useResult
$Res call({bool isRefresh}); $Res call({bool isRefresh, String? status});
} }
/// @nodoc /// @nodoc
@ -102,13 +102,17 @@ class __$$FetchedImplCopyWithImpl<$Res>
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({Object? isRefresh = null}) { $Res call({Object? isRefresh = null, Object? status = freezed}) {
return _then( return _then(
_$FetchedImpl( _$FetchedImpl(
isRefresh: null == isRefresh isRefresh: null == isRefresh
? _value.isRefresh ? _value.isRefresh
: isRefresh // ignore: cast_nullable_to_non_nullable : isRefresh // ignore: cast_nullable_to_non_nullable
as bool, as bool,
status: freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String?,
), ),
); );
} }
@ -117,15 +121,17 @@ class __$$FetchedImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$FetchedImpl implements _Fetched { class _$FetchedImpl implements _Fetched {
const _$FetchedImpl({this.isRefresh = false}); const _$FetchedImpl({this.isRefresh = false, this.status});
@override @override
@JsonKey() @JsonKey()
final bool isRefresh; final bool isRefresh;
@override
final String? status;
@override @override
String toString() { String toString() {
return 'TableLoaderEvent.fetched(isRefresh: $isRefresh)'; return 'TableLoaderEvent.fetched(isRefresh: $isRefresh, status: $status)';
} }
@override @override
@ -134,11 +140,12 @@ class _$FetchedImpl implements _Fetched {
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$FetchedImpl && other is _$FetchedImpl &&
(identical(other.isRefresh, isRefresh) || (identical(other.isRefresh, isRefresh) ||
other.isRefresh == isRefresh)); other.isRefresh == isRefresh) &&
(identical(other.status, status) || other.status == status));
} }
@override @override
int get hashCode => Object.hash(runtimeType, isRefresh); int get hashCode => Object.hash(runtimeType, isRefresh, status);
/// Create a copy of TableLoaderEvent /// Create a copy of TableLoaderEvent
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -151,33 +158,33 @@ class _$FetchedImpl implements _Fetched {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function(bool isRefresh) fetched, required TResult Function(bool isRefresh, String? status) fetched,
required TResult Function(String id, Offset position) updatedPostion, required TResult Function(String id, Offset position) updatedPostion,
required TResult Function(Table? table) setSelectedTable, required TResult Function(Table? table) setSelectedTable,
}) { }) {
return fetched(isRefresh); return fetched(isRefresh, status);
} }
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function(bool isRefresh)? fetched, TResult? Function(bool isRefresh, String? status)? fetched,
TResult? Function(String id, Offset position)? updatedPostion, TResult? Function(String id, Offset position)? updatedPostion,
TResult? Function(Table? table)? setSelectedTable, TResult? Function(Table? table)? setSelectedTable,
}) { }) {
return fetched?.call(isRefresh); return fetched?.call(isRefresh, status);
} }
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function(bool isRefresh)? fetched, TResult Function(bool isRefresh, String? status)? fetched,
TResult Function(String id, Offset position)? updatedPostion, TResult Function(String id, Offset position)? updatedPostion,
TResult Function(Table? table)? setSelectedTable, TResult Function(Table? table)? setSelectedTable,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (fetched != null) { if (fetched != null) {
return fetched(isRefresh); return fetched(isRefresh, status);
} }
return orElse(); return orElse();
} }
@ -218,9 +225,11 @@ class _$FetchedImpl implements _Fetched {
} }
abstract class _Fetched implements TableLoaderEvent { abstract class _Fetched implements TableLoaderEvent {
const factory _Fetched({final bool isRefresh}) = _$FetchedImpl; const factory _Fetched({final bool isRefresh, final String? status}) =
_$FetchedImpl;
bool get isRefresh; bool get isRefresh;
String? get status;
/// Create a copy of TableLoaderEvent /// Create a copy of TableLoaderEvent
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -310,7 +319,7 @@ class _$UpdatedPositionImpl implements _UpdatedPosition {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function(bool isRefresh) fetched, required TResult Function(bool isRefresh, String? status) fetched,
required TResult Function(String id, Offset position) updatedPostion, required TResult Function(String id, Offset position) updatedPostion,
required TResult Function(Table? table) setSelectedTable, required TResult Function(Table? table) setSelectedTable,
}) { }) {
@ -320,7 +329,7 @@ class _$UpdatedPositionImpl implements _UpdatedPosition {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function(bool isRefresh)? fetched, TResult? Function(bool isRefresh, String? status)? fetched,
TResult? Function(String id, Offset position)? updatedPostion, TResult? Function(String id, Offset position)? updatedPostion,
TResult? Function(Table? table)? setSelectedTable, TResult? Function(Table? table)? setSelectedTable,
}) { }) {
@ -330,7 +339,7 @@ class _$UpdatedPositionImpl implements _UpdatedPosition {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function(bool isRefresh)? fetched, TResult Function(bool isRefresh, String? status)? fetched,
TResult Function(String id, Offset position)? updatedPostion, TResult Function(String id, Offset position)? updatedPostion,
TResult Function(Table? table)? setSelectedTable, TResult Function(Table? table)? setSelectedTable,
required TResult orElse(), required TResult orElse(),
@ -481,7 +490,7 @@ class _$SetSelectedTableImpl implements _SetSelectedTable {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function(bool isRefresh) fetched, required TResult Function(bool isRefresh, String? status) fetched,
required TResult Function(String id, Offset position) updatedPostion, required TResult Function(String id, Offset position) updatedPostion,
required TResult Function(Table? table) setSelectedTable, required TResult Function(Table? table) setSelectedTable,
}) { }) {
@ -491,7 +500,7 @@ class _$SetSelectedTableImpl implements _SetSelectedTable {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function(bool isRefresh)? fetched, TResult? Function(bool isRefresh, String? status)? fetched,
TResult? Function(String id, Offset position)? updatedPostion, TResult? Function(String id, Offset position)? updatedPostion,
TResult? Function(Table? table)? setSelectedTable, TResult? Function(Table? table)? setSelectedTable,
}) { }) {
@ -501,7 +510,7 @@ class _$SetSelectedTableImpl implements _SetSelectedTable {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function(bool isRefresh)? fetched, TResult Function(bool isRefresh, String? status)? fetched,
TResult Function(String id, Offset position)? updatedPostion, TResult Function(String id, Offset position)? updatedPostion,
TResult Function(Table? table)? setSelectedTable, TResult Function(Table? table)? setSelectedTable,
required TResult orElse(), required TResult orElse(),

View File

@ -2,8 +2,10 @@ part of 'table_loader_bloc.dart';
@freezed @freezed
class TableLoaderEvent with _$TableLoaderEvent { class TableLoaderEvent with _$TableLoaderEvent {
const factory TableLoaderEvent.fetched({@Default(false) bool isRefresh}) = const factory TableLoaderEvent.fetched({
_Fetched; @Default(false) bool isRefresh,
String? status,
}) = _Fetched;
const factory TableLoaderEvent.updatedPostion({ const factory TableLoaderEvent.updatedPostion({
required String id, required String id,
required Offset position, required Offset position,

View File

@ -4,10 +4,21 @@ abstract class ITableRepository {
Future<Either<TableFailure, ListTable>> fetchTables({ Future<Either<TableFailure, ListTable>> fetchTables({
int page = 1, int page = 1,
int limit = 50, int limit = 50,
String? status,
});
Future<Either<TableFailure, Table>> createTable({
required String name,
required int capacity,
}); });
Future<Either<TableFailure, Table>> updatePosition({ Future<Either<TableFailure, Table>> updatePosition({
required String id, required String id,
required Offset position, required Offset position,
}); });
Future<Either<TableFailure, Unit>> transferTable({
required String fromTableId,
required String toTableId,
});
} }

View File

@ -52,6 +52,7 @@ class OutletRepository implements IOutletRepository {
return left(result.error!); return left(result.error!);
} }
await _localDataProvider.saveCurrentOutlet(result.data!);
final outlet = result.data!.toDomain(); final outlet = result.data!.toDomain();
return right(outlet); return right(outlet);
} catch (e, s) { } catch (e, s) {

View File

@ -1,5 +1,6 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:data_channel/data_channel.dart'; import 'package:data_channel/data_channel.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
@ -21,12 +22,19 @@ class TableRemoteDataProvider {
Future<DC<TableFailure, ListTableDto>> fetchTables({ Future<DC<TableFailure, ListTableDto>> fetchTables({
int page = 1, int page = 1,
int limit = 50, int limit = 50,
String? status,
}) async { }) async {
try { try {
Map<String, dynamic> queryParameters = {'page': page, 'limit': limit};
if (status != null) {
queryParameters['status'] = status;
}
final response = await _apiClient.get( final response = await _apiClient.get(
ApiPath.tables, ApiPath.tables,
headers: getAuthorizationHeader(), headers: getAuthorizationHeader(),
params: {'page': page, 'limit': limit}, params: queryParameters,
); );
if (response.data['data'] == null) { if (response.data['data'] == null) {
@ -44,6 +52,38 @@ class TableRemoteDataProvider {
} }
} }
Future<DC<TableFailure, TableDto>> storeTable({
required String outletId,
required String name,
required int capacity,
}) async {
try {
final response = await _apiClient.post(
ApiPath.tables,
data: {
'outlet_id': outletId,
'table_name': name,
'capacity': capacity,
'position_x': 200,
'position_y': 200,
},
headers: getAuthorizationHeader(),
);
if (response.data['success'] == false) {
return DC.error(TableFailure.unexpectedError());
}
final table = TableDto.fromJson(
response.data['data'] as Map<String, dynamic>,
);
return DC.data(table);
} on ApiFailure catch (e, s) {
log('storeTableError', name: _logName, error: e, stackTrace: s);
return DC.error(TableFailure.serverError(e));
}
}
Future<DC<TableFailure, TableDto>> updatePosition({ Future<DC<TableFailure, TableDto>> updatePosition({
required String id, required String id,
required Offset position, required Offset position,
@ -71,4 +111,26 @@ class TableRemoteDataProvider {
return DC.error(TableFailure.serverError(e)); return DC.error(TableFailure.serverError(e));
} }
} }
Future<DC<TableFailure, Unit>> transferTable({
required String fromTableId,
required String toTableId,
}) async {
try {
final response = await _apiClient.put(
'${ApiPath.tables}/transfer',
data: {'from_table': fromTableId, 'to_table': toTableId},
headers: getAuthorizationHeader(),
);
if (response.data['success'] == false) {
return DC.error(TableFailure.unexpectedError());
}
return DC.data(unit);
} on ApiFailure catch (e, s) {
log('transferTableError', name: _logName, error: e, stackTrace: s);
return DC.error(TableFailure.serverError(e));
}
}
} }

View File

@ -5,24 +5,28 @@ import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import '../../../domain/table/table.dart'; import '../../../domain/table/table.dart';
import '../../outlet/datasources/local_data_provider.dart';
import '../datasources/remote_data_provider.dart'; import '../datasources/remote_data_provider.dart';
@Injectable(as: ITableRepository) @Injectable(as: ITableRepository)
class TableRepository implements ITableRepository { class TableRepository implements ITableRepository {
final TableRemoteDataProvider _remoteDataProvider; final TableRemoteDataProvider _remoteDataProvider;
final OutletLocalDatasource _outletLocalDatasource;
final _logName = 'TableRepository'; final _logName = 'TableRepository';
TableRepository(this._remoteDataProvider); TableRepository(this._remoteDataProvider, this._outletLocalDatasource);
@override @override
Future<Either<TableFailure, ListTable>> fetchTables({ Future<Either<TableFailure, ListTable>> fetchTables({
int page = 1, int page = 1,
int limit = 50, int limit = 50,
String? status,
}) async { }) async {
try { try {
final result = await _remoteDataProvider.fetchTables( final result = await _remoteDataProvider.fetchTables(
page: page, page: page,
limit: limit, limit: limit,
status: status,
); );
if (result.hasError) { if (result.hasError) {
@ -38,6 +42,33 @@ class TableRepository implements ITableRepository {
} }
} }
@override
Future<Either<TableFailure, Table>> createTable({
required String name,
required int capacity,
}) async {
try {
final outlet = await _outletLocalDatasource.currentOutlet();
final result = await _remoteDataProvider.storeTable(
outletId: outlet.id,
name: name,
capacity: capacity,
);
if (result.hasError) {
return left(result.error!);
}
final table = result.data!.toDomain();
return right(table);
} catch (e) {
log('fetchCreateTable', name: _logName, error: e);
return left(const TableFailure.unexpectedError());
}
}
@override @override
Future<Either<TableFailure, Table>> updatePosition({ Future<Either<TableFailure, Table>> updatePosition({
required String id, required String id,
@ -61,4 +92,26 @@ class TableRepository implements ITableRepository {
return left(const TableFailure.unexpectedError()); return left(const TableFailure.unexpectedError());
} }
} }
@override
Future<Either<TableFailure, Unit>> transferTable({
required String fromTableId,
required String toTableId,
}) async {
try {
final result = await _remoteDataProvider.transferTable(
fromTableId: fromTableId,
toTableId: toTableId,
);
if (result.hasError) {
return left(result.error!);
}
return right(unit);
} catch (e) {
log('transferTableError', name: _logName, error: e);
return left(const TableFailure.unexpectedError());
}
}
} }

View File

@ -149,6 +149,18 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i708.CategoryLocalDataProvider>(), gh<_i708.CategoryLocalDataProvider>(),
), ),
); );
gh.factory<_i983.ITableRepository>(
() => _i824.TableRepository(
gh<_i95.TableRemoteDataProvider>(),
gh<_i693.OutletLocalDatasource>(),
),
);
gh.factory<_i248.TableFormBloc>(
() => _i248.TableFormBloc(gh<_i983.ITableRepository>()),
);
gh.factory<_i424.TableLoaderBloc>(
() => _i424.TableLoaderBloc(gh<_i983.ITableRepository>()),
);
gh.factory<_i44.IProductRepository>( gh.factory<_i44.IProductRepository>(
() => _i763.ProductRepository( () => _i763.ProductRepository(
gh<_i707.ProductRemoteDataProvider>(), gh<_i707.ProductRemoteDataProvider>(),
@ -167,9 +179,6 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i1018.CategoryLoaderBloc>( gh.factory<_i1018.CategoryLoaderBloc>(
() => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()), () => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()),
); );
gh.factory<_i983.ITableRepository>(
() => _i824.TableRepository(gh<_i95.TableRemoteDataProvider>()),
);
gh.factory<_i343.AuthBloc>( gh.factory<_i343.AuthBloc>(
() => _i343.AuthBloc( () => _i343.AuthBloc(
gh<_i776.IAuthRepository>(), gh<_i776.IAuthRepository>(),
@ -188,12 +197,6 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i502.ICategoryRepository>(), gh<_i502.ICategoryRepository>(),
), ),
); );
gh.factory<_i424.TableLoaderBloc>(
() => _i424.TableLoaderBloc(gh<_i983.ITableRepository>()),
);
gh.factory<_i248.TableFormBloc>(
() => _i248.TableFormBloc(gh<_i983.ITableRepository>()),
);
return this; return this;
} }
} }

View File

@ -6,6 +6,8 @@ import '../application/category/category_loader/category_loader_bloc.dart';
import '../application/checkout/checkout_form/checkout_form_bloc.dart'; import '../application/checkout/checkout_form/checkout_form_bloc.dart';
import '../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../application/outlet/outlet_loader/outlet_loader_bloc.dart';
import '../application/product/product_loader/product_loader_bloc.dart'; import '../application/product/product_loader/product_loader_bloc.dart';
import '../application/table/table_form/table_form_bloc.dart';
import '../application/table/table_loader/table_loader_bloc.dart';
import '../common/theme/theme.dart'; import '../common/theme/theme.dart';
import '../common/constant/app_constant.dart'; import '../common/constant/app_constant.dart';
import '../injection.dart'; import '../injection.dart';
@ -31,6 +33,8 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()), BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
BlocProvider(create: (context) => getIt<ProductLoaderBloc>()), BlocProvider(create: (context) => getIt<ProductLoaderBloc>()),
BlocProvider(create: (context) => getIt<CheckoutFormBloc>()), BlocProvider(create: (context) => getIt<CheckoutFormBloc>()),
BlocProvider(create: (context) => getIt<TableLoaderBloc>()),
BlocProvider(create: (context) => getIt<TableFormBloc>()),
], ],
child: MaterialApp.router( child: MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/table/table_form/table_form_bloc.dart';
import '../../button/button.dart';
import '../../field/field.dart';
import '../../spaces/space.dart';
import '../dialog.dart';
class TableCreateDialog extends StatelessWidget {
const TableCreateDialog({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<TableFormBloc, TableFormState>(
builder: (context, state) {
return CustomModalDialog(
title: 'Tambah Meja',
subtitle: 'Silahkan isi data meja',
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 24.0,
),
child: Column(
children: [
AppTextFormField(
label: 'Nama Meja',
onChanged: (value) {
context.read<TableFormBloc>().add(
TableFormEvent.nameChanged(value),
);
},
),
SpaceHeight(16),
AppTextFormField(
label: 'Kapasitas',
keyboardType: TextInputType.number,
onChanged: (value) {
context.read<TableFormBloc>().add(
TableFormEvent.capacityChanged(value),
);
},
),
SpaceHeight(24),
AppElevatedButton.filled(
onPressed: () {
context.read<TableFormBloc>().add(
const TableFormEvent.created(),
);
},
label: "Simpan",
isLoading: state.isCreating,
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,284 @@
import 'dart:developer';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/table/table_form/table_form_bloc.dart';
import '../../../../application/table/table_loader/table_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/table/table.dart' as t;
import '../../button/button.dart';
import '../../loader/loader_with_text.dart';
import '../../spaces/space.dart';
import '../../toast/flushbar.dart';
import '../dialog.dart';
class TableTransferDialog extends StatefulWidget {
final t.Table fromTable;
const TableTransferDialog({super.key, required this.fromTable});
@override
State<TableTransferDialog> createState() => _TableTransferDialogState();
}
class _TableTransferDialogState extends State<TableTransferDialog> {
t.Table? selectTable;
@override
void initState() {
super.initState();
context.read<TableLoaderBloc>().add(
TableLoaderEvent.fetched(isRefresh: true, status: 'available'),
);
}
@override
Widget build(BuildContext context) {
return CustomModalDialog(
title: 'Transfer Meja',
subtitle: 'Pilih meja yang tersedia',
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 24.0,
),
minWidth: context.deviceWidth * 0.4,
minHeight: context.deviceHeight * 0.4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pilih Meja',
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w600),
),
const SpaceHeight(6.0),
BlocBuilder<TableLoaderBloc, TableLoaderState>(
builder: (context, state) {
final availableTables = state.tables;
if (state.isFetching) {
return Center(child: const LoaderWithText());
}
if (selectTable == null && availableTables.isNotEmpty) {
selectTable = availableTables.first;
}
if (availableTables.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange[50],
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.orange, width: 1),
),
child: const Text(
'Tidak ada meja yang tersedia. Silakan pilih opsi lain.',
style: TextStyle(color: Colors.orange),
),
);
}
return DropdownSearch<t.Table>(
items: state.tables,
selectedItem: selectTable,
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
hintText: "Pilih meja",
hintStyle: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
prefixIcon: Icon(
Icons.category_outlined,
color: Colors.grey.shade500,
size: 20,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
width: 1.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
width: 1.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.blue.shade400,
width: 2,
),
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
popupProps: PopupProps.menu(
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Cari meja...",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
menuProps: MenuProps(
backgroundColor: Colors.white,
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
itemBuilder: (context, category, isSelected) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: isSelected
? Colors.blue.shade50
: Colors.transparent,
border: Border(
bottom: BorderSide(
color: Colors.grey.shade100,
width: 0.5,
),
),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: isSelected
? Colors.blue.shade600
: Colors.grey.shade400,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
category.tableName,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w500,
color: isSelected
? Colors.blue.shade700
: Colors.black87,
),
),
),
if (isSelected)
Icon(
Icons.check,
color: Colors.blue.shade600,
size: 18,
),
],
),
);
},
emptyBuilder: (context, searchEntry) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.search_off,
color: Colors.grey.shade400,
size: 48,
),
const SizedBox(height: 12),
Text(
searchEntry.isEmpty
? "Tidak ada meja tersedia"
: "Tidak ditemukan meja dengan '$searchEntry'",
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
);
},
),
itemAsString: (t.Table table) => table.tableName,
compareFn: (t.Table? item1, t.Table? item2) {
return item1?.id == item2?.id;
},
onChanged: (t.Table? selectedTable) {
if (selectedTable != null) {
setState(() {
selectTable = selectedTable;
});
log("selectTable: ${selectTable!.tableName}");
}
},
validator: (t.Table? value) {
if (value == null) {
return "Meja harus dipilih";
}
return null;
},
);
},
),
],
),
SpaceHeight(24),
BlocBuilder<TableFormBloc, TableFormState>(
builder: (context, state) {
return AppElevatedButton.filled(
onPressed: () {
if (selectTable == null) {
AppFlushbar.showError(
context,
'Silahkan Pilih Meja Tujuan',
);
return;
}
context.read<TableFormBloc>().add(
TableFormEvent.transfer(
fromTableId: widget.fromTable.id,
toTableId: selectTable!.id,
),
);
},
label: "Transfer",
isLoading: state.isTransfering,
);
},
),
],
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart'; import '../../../common/theme/theme.dart';
import '../../../domain/auth/auth.dart'; import '../../../domain/auth/auth.dart';
import '../../../domain/table/table.dart';
class AppFlushbar { class AppFlushbar {
static void showSuccess(BuildContext context, String message) { static void showSuccess(BuildContext context, String message) {
@ -50,4 +51,18 @@ class AppFlushbar {
unexpectedError: (value) => 'Terjadi kesalahan, silahkan coba lagi', unexpectedError: (value) => 'Terjadi kesalahan, silahkan coba lagi',
), ),
); );
static void showTableFailureToast(
BuildContext context,
TableFailure failure,
) => showError(
context,
failure.map(
serverError: (value) => value.failure.toStringFormatted(context),
dynamicErrorMessage: (value) => value.erroMessage,
unexpectedError: (value) => 'Terjadi kesalahan, silahkan coba lagi',
empty: (value) => 'Tidak ada data',
localStorageError: (value) => 'Terjadi kesalahan, silahkan coba lagi',
),
);
} }

View File

@ -8,7 +8,10 @@ import '../../../../../common/extension/extension.dart';
import '../../../../../common/theme/theme.dart'; import '../../../../../common/theme/theme.dart';
import '../../../../../domain/table/table.dart' as t; import '../../../../../domain/table/table.dart' as t;
import '../../../../../injection.dart'; import '../../../../../injection.dart';
import '../../../../components/dialog/table/table_create_dialog.dart';
import '../../../../components/dialog/table/table_transfer_dialog.dart';
import '../../../../components/loader/loader_with_text.dart'; import '../../../../components/loader/loader_with_text.dart';
import '../../../../components/toast/flushbar.dart';
import 'widgets/floating_bottom_navbar.dart'; import 'widgets/floating_bottom_navbar.dart';
import 'widgets/table_card.dart'; import 'widgets/table_card.dart';
@ -27,7 +30,6 @@ class TablePage extends StatefulWidget implements AutoRouteWrapper {
getIt<TableLoaderBloc>() getIt<TableLoaderBloc>()
..add(TableLoaderEvent.fetched(isRefresh: true)), ..add(TableLoaderEvent.fetched(isRefresh: true)),
), ),
BlocProvider(create: (context) => getIt<TableFormBloc>()),
], ],
child: this, child: this,
); );
@ -43,7 +45,48 @@ class _TablePageState extends State<TablePage> {
final double mapWidth = context.deviceWidth * 2; final double mapWidth = context.deviceWidth * 2;
final double mapHeight = context.deviceHeight * 1.5; final double mapHeight = context.deviceHeight * 1.5;
return Scaffold( return MultiBlocListener(
listeners: [
BlocListener<TableFormBloc, TableFormState>(
listenWhen: (previous, current) =>
previous.failureOrTable != current.failureOrTable,
listener: (context, state) {
state.failureOrTable.fold(() {}, (either) {
either.fold(
(f) => AppFlushbar.showTableFailureToast(context, f),
(success) {
if (context.mounted) {
context.router.maybePop();
context.read<TableLoaderBloc>().add(
TableLoaderEvent.fetched(isRefresh: true),
);
}
},
);
});
},
),
BlocListener<TableFormBloc, TableFormState>(
listenWhen: (previous, current) =>
previous.failureOrTableTransfer != current.failureOrTableTransfer,
listener: (context, state) {
state.failureOrTableTransfer.fold(() {}, (either) {
either.fold(
(f) => AppFlushbar.showTableFailureToast(context, f),
(success) {
if (context.mounted) {
context.router.maybePop();
context.read<TableLoaderBloc>().add(
TableLoaderEvent.fetched(isRefresh: true),
);
}
},
);
});
},
),
],
child: Scaffold(
backgroundColor: AppColor.background, backgroundColor: AppColor.background,
appBar: AppBar( appBar: AppBar(
title: const Text("Layout Meja"), title: const Text("Layout Meja"),
@ -62,10 +105,10 @@ class _TablePageState extends State<TablePage> {
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () { onPressed: () {
// showDialog( showDialog(
// context: context, context: context,
// builder: (context) => FormTableNewDialog(), builder: (context) => TableCreateDialog(),
// ); );
}, },
), ),
], ],
@ -140,7 +183,8 @@ class _TablePageState extends State<TablePage> {
), ),
), ),
...state.tables.map((table) { ...state.tables.map((table) {
final isSelected = state.selectedTable == table; final isSelected =
state.selectedTable == table;
return Positioned( return Positioned(
left: table.positionX, left: table.positionX,
top: table.positionY, top: table.positionY,
@ -213,10 +257,15 @@ class _TablePageState extends State<TablePage> {
), ),
); );
}, },
onTapDown: (details) => onTapDown: (details) => tapPosition =
tapPosition = details.globalPosition, details.globalPosition,
onLongPress: () { onLongPress: () {
if (table.status.isOccupied) { if (table.status.isOccupied) {
context.read<TableLoaderBloc>().add(
TableLoaderEvent.setSelectedTable(
null,
),
);
_showPopupMenu(context, table); _showPopupMenu(context, table);
} }
}, },
@ -241,6 +290,7 @@ class _TablePageState extends State<TablePage> {
); );
}, },
), ),
),
); );
} }
@ -272,10 +322,10 @@ class _TablePageState extends State<TablePage> {
items: [ items: [
PopupMenuItem( PopupMenuItem(
onTap: () { onTap: () {
// showDialog( showDialog(
// context: context, context: context,
// builder: (context) => TransferTableDialog(fromTable: table), builder: (context) => TableTransferDialog(fromTable: table),
// ); );
}, },
child: Row( child: Row(
children: [ children: [

View File

@ -345,6 +345,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
dropdown_search:
dependency: "direct main"
description:
name: dropdown_search
sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:

View File

@ -36,6 +36,7 @@ dependencies:
sqflite: ^2.4.2 sqflite: ^2.4.2
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
shimmer: ^3.0.0 shimmer: ^3.0.0
dropdown_search: ^5.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: