checkout form

This commit is contained in:
efrilm 2025-10-25 02:09:47 +07:00
parent e85138f27e
commit 013f313e35
13 changed files with 1719 additions and 4 deletions

BIN
assets/images/gojek.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
assets/images/grab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -0,0 +1,168 @@
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../common/types/order_type.dart';
import '../../../domain/delivery/delivery.dart';
import '../../../domain/product/product.dart';
part 'checkout_form_event.dart';
part 'checkout_form_state.dart';
part 'checkout_form_bloc.freezed.dart';
@injectable
class CheckoutFormBloc extends Bloc<CheckoutFormEvent, CheckoutFormState> {
CheckoutFormBloc() : super(CheckoutFormState.initial()) {
on<CheckoutFormEvent>(_onCheckoutFormEvent);
}
Future<void> _onCheckoutFormEvent(
CheckoutFormEvent event,
Emitter<CheckoutFormState> emit,
) {
return event.map(
addItem: (e) async {
final currentState = state;
emit(currentState.copyWith(isLoading: true));
List<ProductQuantity> items = [...currentState.items];
final index = items.indexWhere(
(element) =>
element.product.id == e.product.id &&
element.variant?.id == e.variant?.id,
);
if (index != -1) {
// Jika sudah ada tambah quantity
items[index] = items[index].copyWith(
quantity: items[index].quantity + 1,
);
} else {
// Jika belum ada tambahkan item baru
items.add(
ProductQuantity(
product: e.product,
quantity: 1,
variant: e.variant,
),
);
}
log('🛒 Items updated: ${items.length} items total');
final totalQuantity = items.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
final totalPrice = items.fold<int>(
0,
(sum, item) =>
sum +
(item.quantity *
(item.variant?.priceModifier.toInt() ??
item.product.price.toInt())),
);
emit(
currentState.copyWith(
items: items,
totalQuantity: totalQuantity,
totalPrice: totalPrice,
isLoading: false,
),
);
},
removeItem: (e) async {
final currentState = state;
emit(currentState.copyWith(isLoading: true));
List<ProductQuantity> items = [...currentState.items];
final index = items.indexWhere(
(element) =>
element.product.id == e.product.id &&
element.variant?.id == e.variant?.id,
);
if (index != -1) {
final currentItem = items[index];
if (currentItem.quantity > 1) {
// Kurangi quantity
items[index] = currentItem.copyWith(
quantity: currentItem.quantity - 1,
);
} else {
// Hapus item kalau quantity = 1
items.removeAt(index);
}
}
final totalQuantity = items.fold<int>(
0,
(sum, item) => sum + item.quantity,
);
final totalPrice = items.fold<int>(
0,
(sum, item) =>
sum +
(item.quantity *
(item.variant?.priceModifier.toInt() ??
item.product.price.toInt())),
);
log(
'🗑️ Item removed. Total items: ${items.length}, totalQuantity: $totalQuantity, totalPrice: $totalPrice',
);
// Emit state baru
emit(
currentState.copyWith(
items: items,
totalQuantity: totalQuantity,
totalPrice: totalPrice,
isLoading: false,
),
);
},
started: (e) async {
emit(CheckoutFormState.initial().copyWith(isLoading: true));
try {
emit(
CheckoutFormState.initial().copyWith(
items: e.items,
tax: 0,
serviceCharge: 0,
isLoading: false,
),
);
} catch (e) {
// Kalau gagal, pakai default values
log('⚠️ Failed to load settings: $e');
emit(
CheckoutFormState.initial().copyWith(
tax: 10,
serviceCharge: 5,
isLoading: false,
),
);
}
},
updateItemNotes: (e) async {
final currentState = state;
// Clone list items agar tidak mutasi langsung
final items = [...currentState.items];
final index = items.indexWhere(
(element) => element.product.id == e.product.id,
);
if (index != -1) {
items[index] = items[index].copyWith(notes: e.notes);
}
emit(currentState.copyWith(items: items, isLoading: false));
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
part of 'checkout_form_bloc.dart';
@freezed
class CheckoutFormEvent with _$CheckoutFormEvent {
const factory CheckoutFormEvent.started(List<ProductQuantity> items) =
_Started;
const factory CheckoutFormEvent.addItem(
Product product,
ProductVariant? variant,
) = _AddItem;
const factory CheckoutFormEvent.removeItem(
Product product,
ProductVariant? variant,
) = _RemoveItem;
const factory CheckoutFormEvent.updateItemNotes(
Product product,
String notes,
) = _UpdateItemNotes;
}

View File

@ -0,0 +1,30 @@
part of 'checkout_form_bloc.dart';
@freezed
class CheckoutFormState with _$CheckoutFormState {
factory CheckoutFormState({
required List<ProductQuantity> items,
required int discount,
required int discountAmount,
required int tax,
required int serviceCharge,
required int totalQuantity,
required int totalPrice,
required String draftName,
required OrderType orderType,
Delivery? delivery,
@Default(false) bool isLoading,
}) = _CheckoutFormState;
factory CheckoutFormState.initial() => CheckoutFormState(
items: [],
discount: 0,
discountAmount: 0,
tax: 0,
serviceCharge: 0,
totalQuantity: 0,
totalPrice: 0,
draftName: '',
orderType: OrderType.dineIn,
);
}

View File

@ -0,0 +1,16 @@
enum OrderType {
dineIn('DINE IN'),
takeAway('TAKE AWAY'),
delivery('DELIVERY'),
freeTable('FREE TABLE');
final String value;
const OrderType(this.value);
static OrderType fromString(String value) {
return OrderType.values.firstWhere(
(type) => type.value == value,
orElse: () => OrderType.dineIn,
);
}
}

View File

@ -0,0 +1,14 @@
import '../../presentation/components/assets/assets.gen.dart';
class Delivery {
String id;
String name;
String imageUrl;
Delivery({required this.id, required this.name, required this.imageUrl});
}
List<Delivery> deliveries = [
Delivery(id: 'gojek', name: 'Gojek', imageUrl: Assets.images.gojek.path),
Delivery(id: 'grab', name: 'Grab', imageUrl: Assets.images.grab.path),
];

View File

@ -0,0 +1,14 @@
part of '../product.dart';
@freezed
class ProductQuantity with _$ProductQuantity {
const factory ProductQuantity({
required Product product,
ProductVariant? variant,
required int quantity,
String? notes,
}) = _ProductQuantity;
factory ProductQuantity.empty() =>
ProductQuantity(product: Product.empty(), quantity: 0);
}

View File

@ -7,5 +7,6 @@ import '../../common/api/api_failure.dart';
part 'product.freezed.dart';
part 'entities/product_entity.dart';
part 'entities/product_quantity_entity.dart';
part 'failures/product_failure.dart';
part 'repositories/i_product_repository.dart';

View File

@ -1092,6 +1092,260 @@ abstract class _ProductVariant implements ProductVariant {
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProductQuantity {
Product get product => throw _privateConstructorUsedError;
ProductVariant? get variant => throw _privateConstructorUsedError;
int get quantity => throw _privateConstructorUsedError;
String? get notes => throw _privateConstructorUsedError;
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProductQuantityCopyWith<ProductQuantity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProductQuantityCopyWith<$Res> {
factory $ProductQuantityCopyWith(
ProductQuantity value,
$Res Function(ProductQuantity) then,
) = _$ProductQuantityCopyWithImpl<$Res, ProductQuantity>;
@useResult
$Res call({
Product product,
ProductVariant? variant,
int quantity,
String? notes,
});
$ProductCopyWith<$Res> get product;
$ProductVariantCopyWith<$Res>? get variant;
}
/// @nodoc
class _$ProductQuantityCopyWithImpl<$Res, $Val extends ProductQuantity>
implements $ProductQuantityCopyWith<$Res> {
_$ProductQuantityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? product = null,
Object? variant = freezed,
Object? quantity = null,
Object? notes = freezed,
}) {
return _then(
_value.copyWith(
product: null == product
? _value.product
: product // ignore: cast_nullable_to_non_nullable
as Product,
variant: freezed == variant
? _value.variant
: variant // ignore: cast_nullable_to_non_nullable
as ProductVariant?,
quantity: null == quantity
? _value.quantity
: quantity // ignore: cast_nullable_to_non_nullable
as int,
notes: freezed == notes
? _value.notes
: notes // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProductCopyWith<$Res> get product {
return $ProductCopyWith<$Res>(_value.product, (value) {
return _then(_value.copyWith(product: value) as $Val);
});
}
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProductVariantCopyWith<$Res>? get variant {
if (_value.variant == null) {
return null;
}
return $ProductVariantCopyWith<$Res>(_value.variant!, (value) {
return _then(_value.copyWith(variant: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ProductQuantityImplCopyWith<$Res>
implements $ProductQuantityCopyWith<$Res> {
factory _$$ProductQuantityImplCopyWith(
_$ProductQuantityImpl value,
$Res Function(_$ProductQuantityImpl) then,
) = __$$ProductQuantityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
Product product,
ProductVariant? variant,
int quantity,
String? notes,
});
@override
$ProductCopyWith<$Res> get product;
@override
$ProductVariantCopyWith<$Res>? get variant;
}
/// @nodoc
class __$$ProductQuantityImplCopyWithImpl<$Res>
extends _$ProductQuantityCopyWithImpl<$Res, _$ProductQuantityImpl>
implements _$$ProductQuantityImplCopyWith<$Res> {
__$$ProductQuantityImplCopyWithImpl(
_$ProductQuantityImpl _value,
$Res Function(_$ProductQuantityImpl) _then,
) : super(_value, _then);
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? product = null,
Object? variant = freezed,
Object? quantity = null,
Object? notes = freezed,
}) {
return _then(
_$ProductQuantityImpl(
product: null == product
? _value.product
: product // ignore: cast_nullable_to_non_nullable
as Product,
variant: freezed == variant
? _value.variant
: variant // ignore: cast_nullable_to_non_nullable
as ProductVariant?,
quantity: null == quantity
? _value.quantity
: quantity // ignore: cast_nullable_to_non_nullable
as int,
notes: freezed == notes
? _value.notes
: notes // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$ProductQuantityImpl
with DiagnosticableTreeMixin
implements _ProductQuantity {
const _$ProductQuantityImpl({
required this.product,
this.variant,
required this.quantity,
this.notes,
});
@override
final Product product;
@override
final ProductVariant? variant;
@override
final int quantity;
@override
final String? notes;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'ProductQuantity(product: $product, variant: $variant, quantity: $quantity, notes: $notes)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'ProductQuantity'))
..add(DiagnosticsProperty('product', product))
..add(DiagnosticsProperty('variant', variant))
..add(DiagnosticsProperty('quantity', quantity))
..add(DiagnosticsProperty('notes', notes));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProductQuantityImpl &&
(identical(other.product, product) || other.product == product) &&
(identical(other.variant, variant) || other.variant == variant) &&
(identical(other.quantity, quantity) ||
other.quantity == quantity) &&
(identical(other.notes, notes) || other.notes == notes));
}
@override
int get hashCode =>
Object.hash(runtimeType, product, variant, quantity, notes);
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProductQuantityImplCopyWith<_$ProductQuantityImpl> get copyWith =>
__$$ProductQuantityImplCopyWithImpl<_$ProductQuantityImpl>(
this,
_$identity,
);
}
abstract class _ProductQuantity implements ProductQuantity {
const factory _ProductQuantity({
required final Product product,
final ProductVariant? variant,
required final int quantity,
final String? notes,
}) = _$ProductQuantityImpl;
@override
Product get product;
@override
ProductVariant? get variant;
@override
int get quantity;
@override
String? get notes;
/// Create a copy of ProductQuantity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProductQuantityImplCopyWith<_$ProductQuantityImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProductFailure {
@optionalTypeArgs

View File

@ -14,6 +14,8 @@ import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_blo
as _i46;
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'
as _i13;
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
as _i76;
import 'package:apskel_pos_flutter_v2/application/product/product_loader/product_loader_bloc.dart'
@ -86,6 +88,7 @@ extension GetItInjectableX on _i174.GetIt {
() => sharedPreferencesDi.prefs,
preResolve: true,
);
gh.factory<_i13.CheckoutFormBloc>(() => _i13.CheckoutFormBloc());
gh.singleton<_i487.DatabaseHelper>(() => databaseDi.databaseHelper);
gh.lazySingleton<_i361.Dio>(() => dioDi.dio);
gh.lazySingleton<_i800.AppRouter>(() => autoRouteDi.appRouter);
@ -116,12 +119,12 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i370.AuthRemoteDataProvider>(
() => _i370.AuthRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i132.OutletRemoteDataProvider>(
() => _i132.OutletRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i707.ProductRemoteDataProvider>(
() => _i707.ProductRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i132.OutletRemoteDataProvider>(
() => _i132.OutletRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i776.IAuthRepository>(
() => _i941.AuthRepository(
gh<_i370.AuthRemoteDataProvider>(),

View File

@ -14,6 +14,12 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/gojek.png
AssetGenImage get gojek => const AssetGenImage('assets/images/gojek.png');
/// File path: assets/images/grab.png
AssetGenImage get grab => const AssetGenImage('assets/images/grab.png');
/// File path: assets/images/logo.png
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
@ -22,7 +28,7 @@ class $AssetsImagesGen {
const AssetGenImage('assets/images/logo_white.png');
/// List of all assets
List<AssetGenImage> get values => [logo, logoWhite];
List<AssetGenImage> get values => [gojek, grab, logo, logoWhite];
}
class Assets {