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,
) {
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;
emit(state.copyWith(isUpdating: true, failureOrTable: none()));
emit(state.copyWith(isCreating: true, failureOrTable: none()));
failureOrTable = await _repository.updatePosition(
id: e.id,
position: e.position,
final isNameValid = state.name.isNotEmpty;
final isCapacityValid = state.capacity > 0;
if (isNameValid && isCapacityValid) {
failureOrTable = await _repository.createTable(
name: state.name,
capacity: state.capacity,
);
emit(
state.copyWith(
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,
),
);
},
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
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({
required String id,
required Offset position,
}) = _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
class TableFormState with _$TableFormState {
factory TableFormState({
required String name,
required int capacity,
required Option<Either<TableFailure, Table>> failureOrTable,
required Option<Either<TableFailure, Unit>> failureOrTableTransfer,
@Default(false) bool isUpdating,
@Default(false) bool isCreating,
@Default(false) bool isTransfering,
}) = _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);
}
newState = await _mapFetchedToState(newState, isRefresh: e.isRefresh);
newState = await _mapFetchedToState(
newState,
isRefresh: e.isRefresh,
status: e.status,
);
emit(newState);
},
updatedPostion: (e) async {
@ -62,6 +66,7 @@ class TableLoaderBloc extends Bloc<TableLoaderEvent, TableLoaderState> {
Future<TableLoaderState> _mapFetchedToState(
TableLoaderState state, {
bool isRefresh = false,
String? status,
}) async {
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(
(f) {

View File

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

View File

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

View File

@ -4,10 +4,21 @@ abstract class ITableRepository {
Future<Either<TableFailure, ListTable>> fetchTables({
int page = 1,
int limit = 50,
String? status,
});
Future<Either<TableFailure, Table>> createTable({
required String name,
required int capacity,
});
Future<Either<TableFailure, Table>> updatePosition({
required String id,
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!);
}
await _localDataProvider.saveCurrentOutlet(result.data!);
final outlet = result.data!.toDomain();
return right(outlet);
} catch (e, s) {

View File

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:data_channel/data_channel.dart';
import 'package:flutter/widgets.dart';
import 'package:injectable/injectable.dart';
@ -21,12 +22,19 @@ class TableRemoteDataProvider {
Future<DC<TableFailure, ListTableDto>> fetchTables({
int page = 1,
int limit = 50,
String? status,
}) async {
try {
Map<String, dynamic> queryParameters = {'page': page, 'limit': limit};
if (status != null) {
queryParameters['status'] = status;
}
final response = await _apiClient.get(
ApiPath.tables,
headers: getAuthorizationHeader(),
params: {'page': page, 'limit': limit},
params: queryParameters,
);
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({
required String id,
required Offset position,
@ -71,4 +111,26 @@ class TableRemoteDataProvider {
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 '../../../domain/table/table.dart';
import '../../outlet/datasources/local_data_provider.dart';
import '../datasources/remote_data_provider.dart';
@Injectable(as: ITableRepository)
class TableRepository implements ITableRepository {
final TableRemoteDataProvider _remoteDataProvider;
final OutletLocalDatasource _outletLocalDatasource;
final _logName = 'TableRepository';
TableRepository(this._remoteDataProvider);
TableRepository(this._remoteDataProvider, this._outletLocalDatasource);
@override
Future<Either<TableFailure, ListTable>> fetchTables({
int page = 1,
int limit = 50,
String? status,
}) async {
try {
final result = await _remoteDataProvider.fetchTables(
page: page,
limit: limit,
status: status,
);
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
Future<Either<TableFailure, Table>> updatePosition({
required String id,
@ -61,4 +92,26 @@ class TableRepository implements ITableRepository {
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.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>(
() => _i763.ProductRepository(
gh<_i707.ProductRemoteDataProvider>(),
@ -167,9 +179,6 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i1018.CategoryLoaderBloc>(
() => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()),
);
gh.factory<_i983.ITableRepository>(
() => _i824.TableRepository(gh<_i95.TableRemoteDataProvider>()),
);
gh.factory<_i343.AuthBloc>(
() => _i343.AuthBloc(
gh<_i776.IAuthRepository>(),
@ -188,12 +197,6 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i502.ICategoryRepository>(),
),
);
gh.factory<_i424.TableLoaderBloc>(
() => _i424.TableLoaderBloc(gh<_i983.ITableRepository>()),
);
gh.factory<_i248.TableFormBloc>(
() => _i248.TableFormBloc(gh<_i983.ITableRepository>()),
);
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/outlet/outlet_loader/outlet_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/constant/app_constant.dart';
import '../injection.dart';
@ -31,6 +33,8 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
BlocProvider(create: (context) => getIt<ProductLoaderBloc>()),
BlocProvider(create: (context) => getIt<CheckoutFormBloc>()),
BlocProvider(create: (context) => getIt<TableLoaderBloc>()),
BlocProvider(create: (context) => getIt<TableFormBloc>()),
],
child: MaterialApp.router(
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 '../../../domain/auth/auth.dart';
import '../../../domain/table/table.dart';
class AppFlushbar {
static void showSuccess(BuildContext context, String message) {
@ -50,4 +51,18 @@ class AppFlushbar {
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 '../../../../../domain/table/table.dart' as t;
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/toast/flushbar.dart';
import 'widgets/floating_bottom_navbar.dart';
import 'widgets/table_card.dart';
@ -27,7 +30,6 @@ class TablePage extends StatefulWidget implements AutoRouteWrapper {
getIt<TableLoaderBloc>()
..add(TableLoaderEvent.fetched(isRefresh: true)),
),
BlocProvider(create: (context) => getIt<TableFormBloc>()),
],
child: this,
);
@ -43,7 +45,48 @@ class _TablePageState extends State<TablePage> {
final double mapWidth = context.deviceWidth * 2;
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,
appBar: AppBar(
title: const Text("Layout Meja"),
@ -62,10 +105,10 @@ class _TablePageState extends State<TablePage> {
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
// showDialog(
// context: context,
// builder: (context) => FormTableNewDialog(),
// );
showDialog(
context: context,
builder: (context) => TableCreateDialog(),
);
},
),
],
@ -140,7 +183,8 @@ class _TablePageState extends State<TablePage> {
),
),
...state.tables.map((table) {
final isSelected = state.selectedTable == table;
final isSelected =
state.selectedTable == table;
return Positioned(
left: table.positionX,
top: table.positionY,
@ -213,10 +257,15 @@ class _TablePageState extends State<TablePage> {
),
);
},
onTapDown: (details) =>
tapPosition = details.globalPosition,
onTapDown: (details) => tapPosition =
details.globalPosition,
onLongPress: () {
if (table.status.isOccupied) {
context.read<TableLoaderBloc>().add(
TableLoaderEvent.setSelectedTable(
null,
),
);
_showPopupMenu(context, table);
}
},
@ -241,6 +290,7 @@ class _TablePageState extends State<TablePage> {
);
},
),
),
);
}
@ -272,10 +322,10 @@ class _TablePageState extends State<TablePage> {
items: [
PopupMenuItem(
onTap: () {
// showDialog(
// context: context,
// builder: (context) => TransferTableDialog(fromTable: table),
// );
showDialog(
context: context,
builder: (context) => TableTransferDialog(fromTable: table),
);
},
child: Row(
children: [

View File

@ -345,6 +345,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

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