diff --git a/lib/application/customer/customer_loader/customer_loader_bloc.dart b/lib/application/customer/customer_loader/customer_loader_bloc.dart new file mode 100644 index 0000000..a176b56 --- /dev/null +++ b/lib/application/customer/customer_loader/customer_loader_bloc.dart @@ -0,0 +1,87 @@ +import 'package:dartz/dartz.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/customer/customer.dart'; + +part 'customer_loader_event.dart'; +part 'customer_loader_state.dart'; +part 'customer_loader_bloc.freezed.dart'; + +@injectable +class CustomerLoaderBloc + extends Bloc { + final ICustomerRepository _repository; + CustomerLoaderBloc(this._repository) : super(CustomerLoaderState.initial()) { + on(_onCustomerLoaderEvent); + } + + Future _onCustomerLoaderEvent( + CustomerLoaderEvent event, + Emitter emit, + ) { + return event.map( + searchChanged: (e) async { + emit(state.copyWith(search: e.search)); + }, + fetched: (e) async { + var newState = state; + + if (e.isRefresh) { + newState = state.copyWith(isFetching: true); + + emit(newState); + } + + newState = await _mapFetchedToState(state, isRefresh: e.isRefresh); + + emit(newState); + }, + ); + } + + Future _mapFetchedToState( + CustomerLoaderState state, { + bool isRefresh = false, + }) async { + state = state.copyWith(isFetching: false); + + if (state.hasReachedMax && state.customers.isNotEmpty && !isRefresh) { + return state; + } + + if (isRefresh) { + state = state.copyWith( + page: 1, + failureOptionCustomer: none(), + hasReachedMax: false, + customers: [], + ); + } + + final failureOrCustomer = await _repository.get( + page: state.page, + search: state.search, + ); + + state = failureOrCustomer.fold( + (f) { + if (state.customers.isNotEmpty) { + return state.copyWith(hasReachedMax: true); + } + return state.copyWith(failureOptionCustomer: optionOf(f)); + }, + (customers) { + return state.copyWith( + customers: List.from(state.customers)..addAll(customers), + failureOptionCustomer: none(), + page: state.page + 1, + hasReachedMax: customers.length < 10, + ); + }, + ); + + return state; + } +} diff --git a/lib/application/customer/customer_loader/customer_loader_bloc.freezed.dart b/lib/application/customer/customer_loader/customer_loader_bloc.freezed.dart new file mode 100644 index 0000000..d612039 --- /dev/null +++ b/lib/application/customer/customer_loader/customer_loader_bloc.freezed.dart @@ -0,0 +1,653 @@ +// 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 'customer_loader_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 _$CustomerLoaderEvent { + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool isRefresh) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool isRefresh)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_Fetched value) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_Fetched value)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerLoaderEventCopyWith<$Res> { + factory $CustomerLoaderEventCopyWith( + CustomerLoaderEvent value, + $Res Function(CustomerLoaderEvent) then, + ) = _$CustomerLoaderEventCopyWithImpl<$Res, CustomerLoaderEvent>; +} + +/// @nodoc +class _$CustomerLoaderEventCopyWithImpl<$Res, $Val extends CustomerLoaderEvent> + implements $CustomerLoaderEventCopyWith<$Res> { + _$CustomerLoaderEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$SearchChangedImplCopyWith<$Res> { + factory _$$SearchChangedImplCopyWith( + _$SearchChangedImpl value, + $Res Function(_$SearchChangedImpl) then, + ) = __$$SearchChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({String search}); +} + +/// @nodoc +class __$$SearchChangedImplCopyWithImpl<$Res> + extends _$CustomerLoaderEventCopyWithImpl<$Res, _$SearchChangedImpl> + implements _$$SearchChangedImplCopyWith<$Res> { + __$$SearchChangedImplCopyWithImpl( + _$SearchChangedImpl _value, + $Res Function(_$SearchChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? search = null}) { + return _then( + _$SearchChangedImpl( + null == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$SearchChangedImpl implements _SearchChanged { + const _$SearchChangedImpl(this.search); + + @override + final String search; + + @override + String toString() { + return 'CustomerLoaderEvent.searchChanged(search: $search)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SearchChangedImpl && + (identical(other.search, search) || other.search == search)); + } + + @override + int get hashCode => Object.hash(runtimeType, search); + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SearchChangedImplCopyWith<_$SearchChangedImpl> get copyWith => + __$$SearchChangedImplCopyWithImpl<_$SearchChangedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool isRefresh) fetched, + }) { + return searchChanged(search); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool isRefresh)? fetched, + }) { + return searchChanged?.call(search); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) { + if (searchChanged != null) { + return searchChanged(search); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_Fetched value) fetched, + }) { + return searchChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_Fetched value)? fetched, + }) { + return searchChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (searchChanged != null) { + return searchChanged(this); + } + return orElse(); + } +} + +abstract class _SearchChanged implements CustomerLoaderEvent { + const factory _SearchChanged(final String search) = _$SearchChangedImpl; + + String get search; + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SearchChangedImplCopyWith<_$SearchChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$FetchedImplCopyWith<$Res> { + factory _$$FetchedImplCopyWith( + _$FetchedImpl value, + $Res Function(_$FetchedImpl) then, + ) = __$$FetchedImplCopyWithImpl<$Res>; + @useResult + $Res call({bool isRefresh}); +} + +/// @nodoc +class __$$FetchedImplCopyWithImpl<$Res> + extends _$CustomerLoaderEventCopyWithImpl<$Res, _$FetchedImpl> + implements _$$FetchedImplCopyWith<$Res> { + __$$FetchedImplCopyWithImpl( + _$FetchedImpl _value, + $Res Function(_$FetchedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? isRefresh = null}) { + return _then( + _$FetchedImpl( + isRefresh: null == isRefresh + ? _value.isRefresh + : isRefresh // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$FetchedImpl implements _Fetched { + const _$FetchedImpl({this.isRefresh = false}); + + @override + @JsonKey() + final bool isRefresh; + + @override + String toString() { + return 'CustomerLoaderEvent.fetched(isRefresh: $isRefresh)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FetchedImpl && + (identical(other.isRefresh, isRefresh) || + other.isRefresh == isRefresh)); + } + + @override + int get hashCode => Object.hash(runtimeType, isRefresh); + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FetchedImplCopyWith<_$FetchedImpl> get copyWith => + __$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool isRefresh) fetched, + }) { + return fetched(isRefresh); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool isRefresh)? fetched, + }) { + return fetched?.call(isRefresh); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(isRefresh); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_Fetched value) fetched, + }) { + return fetched(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_Fetched value)? fetched, + }) { + return fetched?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(this); + } + return orElse(); + } +} + +abstract class _Fetched implements CustomerLoaderEvent { + const factory _Fetched({final bool isRefresh}) = _$FetchedImpl; + + bool get isRefresh; + + /// Create a copy of CustomerLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FetchedImplCopyWith<_$FetchedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$CustomerLoaderState { + List get customers => throw _privateConstructorUsedError; + Option get failureOptionCustomer => + throw _privateConstructorUsedError; + String? get categoryId => throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + bool get hasReachedMax => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + + /// Create a copy of CustomerLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CustomerLoaderStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerLoaderStateCopyWith<$Res> { + factory $CustomerLoaderStateCopyWith( + CustomerLoaderState value, + $Res Function(CustomerLoaderState) then, + ) = _$CustomerLoaderStateCopyWithImpl<$Res, CustomerLoaderState>; + @useResult + $Res call({ + List customers, + Option failureOptionCustomer, + String? categoryId, + String? search, + bool isFetching, + bool hasReachedMax, + int page, + }); +} + +/// @nodoc +class _$CustomerLoaderStateCopyWithImpl<$Res, $Val extends CustomerLoaderState> + implements $CustomerLoaderStateCopyWith<$Res> { + _$CustomerLoaderStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CustomerLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? customers = null, + Object? failureOptionCustomer = null, + Object? categoryId = freezed, + Object? search = freezed, + Object? isFetching = null, + Object? hasReachedMax = null, + Object? page = null, + }) { + return _then( + _value.copyWith( + customers: null == customers + ? _value.customers + : customers // ignore: cast_nullable_to_non_nullable + as List, + failureOptionCustomer: null == failureOptionCustomer + ? _value.failureOptionCustomer + : failureOptionCustomer // ignore: cast_nullable_to_non_nullable + as Option, + categoryId: freezed == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String?, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + hasReachedMax: null == hasReachedMax + ? _value.hasReachedMax + : hasReachedMax // ignore: cast_nullable_to_non_nullable + as bool, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CustomerLoaderStateImplCopyWith<$Res> + implements $CustomerLoaderStateCopyWith<$Res> { + factory _$$CustomerLoaderStateImplCopyWith( + _$CustomerLoaderStateImpl value, + $Res Function(_$CustomerLoaderStateImpl) then, + ) = __$$CustomerLoaderStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + List customers, + Option failureOptionCustomer, + String? categoryId, + String? search, + bool isFetching, + bool hasReachedMax, + int page, + }); +} + +/// @nodoc +class __$$CustomerLoaderStateImplCopyWithImpl<$Res> + extends _$CustomerLoaderStateCopyWithImpl<$Res, _$CustomerLoaderStateImpl> + implements _$$CustomerLoaderStateImplCopyWith<$Res> { + __$$CustomerLoaderStateImplCopyWithImpl( + _$CustomerLoaderStateImpl _value, + $Res Function(_$CustomerLoaderStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? customers = null, + Object? failureOptionCustomer = null, + Object? categoryId = freezed, + Object? search = freezed, + Object? isFetching = null, + Object? hasReachedMax = null, + Object? page = null, + }) { + return _then( + _$CustomerLoaderStateImpl( + customers: null == customers + ? _value._customers + : customers // ignore: cast_nullable_to_non_nullable + as List, + failureOptionCustomer: null == failureOptionCustomer + ? _value.failureOptionCustomer + : failureOptionCustomer // ignore: cast_nullable_to_non_nullable + as Option, + categoryId: freezed == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String?, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + hasReachedMax: null == hasReachedMax + ? _value.hasReachedMax + : hasReachedMax // ignore: cast_nullable_to_non_nullable + as bool, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc + +class _$CustomerLoaderStateImpl implements _CustomerLoaderState { + const _$CustomerLoaderStateImpl({ + required final List customers, + required this.failureOptionCustomer, + this.categoryId, + this.search, + this.isFetching = false, + this.hasReachedMax = false, + this.page = 1, + }) : _customers = customers; + + final List _customers; + @override + List get customers { + if (_customers is EqualUnmodifiableListView) return _customers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_customers); + } + + @override + final Option failureOptionCustomer; + @override + final String? categoryId; + @override + final String? search; + @override + @JsonKey() + final bool isFetching; + @override + @JsonKey() + final bool hasReachedMax; + @override + @JsonKey() + final int page; + + @override + String toString() { + return 'CustomerLoaderState(customers: $customers, failureOptionCustomer: $failureOptionCustomer, categoryId: $categoryId, search: $search, isFetching: $isFetching, hasReachedMax: $hasReachedMax, page: $page)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CustomerLoaderStateImpl && + const DeepCollectionEquality().equals( + other._customers, + _customers, + ) && + (identical(other.failureOptionCustomer, failureOptionCustomer) || + other.failureOptionCustomer == failureOptionCustomer) && + (identical(other.categoryId, categoryId) || + other.categoryId == categoryId) && + (identical(other.search, search) || other.search == search) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching) && + (identical(other.hasReachedMax, hasReachedMax) || + other.hasReachedMax == hasReachedMax) && + (identical(other.page, page) || other.page == page)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_customers), + failureOptionCustomer, + categoryId, + search, + isFetching, + hasReachedMax, + page, + ); + + /// Create a copy of CustomerLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CustomerLoaderStateImplCopyWith<_$CustomerLoaderStateImpl> get copyWith => + __$$CustomerLoaderStateImplCopyWithImpl<_$CustomerLoaderStateImpl>( + this, + _$identity, + ); +} + +abstract class _CustomerLoaderState implements CustomerLoaderState { + const factory _CustomerLoaderState({ + required final List customers, + required final Option failureOptionCustomer, + final String? categoryId, + final String? search, + final bool isFetching, + final bool hasReachedMax, + final int page, + }) = _$CustomerLoaderStateImpl; + + @override + List get customers; + @override + Option get failureOptionCustomer; + @override + String? get categoryId; + @override + String? get search; + @override + bool get isFetching; + @override + bool get hasReachedMax; + @override + int get page; + + /// Create a copy of CustomerLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CustomerLoaderStateImplCopyWith<_$CustomerLoaderStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/application/customer/customer_loader/customer_loader_event.dart b/lib/application/customer/customer_loader/customer_loader_event.dart new file mode 100644 index 0000000..82ebd41 --- /dev/null +++ b/lib/application/customer/customer_loader/customer_loader_event.dart @@ -0,0 +1,9 @@ +part of 'customer_loader_bloc.dart'; + +@freezed +class CustomerLoaderEvent with _$CustomerLoaderEvent { + const factory CustomerLoaderEvent.searchChanged(String search) = + _SearchChanged; + const factory CustomerLoaderEvent.fetched({@Default(false) bool isRefresh}) = + _Fetched; +} diff --git a/lib/application/customer/customer_loader/customer_loader_state.dart b/lib/application/customer/customer_loader/customer_loader_state.dart new file mode 100644 index 0000000..4cad298 --- /dev/null +++ b/lib/application/customer/customer_loader/customer_loader_state.dart @@ -0,0 +1,17 @@ +part of 'customer_loader_bloc.dart'; + +@freezed +class CustomerLoaderState with _$CustomerLoaderState { + const factory CustomerLoaderState({ + required List customers, + required Option failureOptionCustomer, + String? categoryId, + String? search, + @Default(false) bool isFetching, + @Default(false) bool hasReachedMax, + @Default(1) int page, + }) = _CustomerLoaderState; + + factory CustomerLoaderState.initial() => + CustomerLoaderState(customers: [], failureOptionCustomer: none()); +} diff --git a/lib/common/url/api_path.dart b/lib/common/url/api_path.dart index ad89d04..1c2acee 100644 --- a/lib/common/url/api_path.dart +++ b/lib/common/url/api_path.dart @@ -17,4 +17,7 @@ class ApiPath { // Product static const String product = '/api/v1/products'; + + // Customer + static const String customer = '/api/v1/customers'; } diff --git a/lib/domain/customer/customer.dart b/lib/domain/customer/customer.dart new file mode 100644 index 0000000..d81a9f0 --- /dev/null +++ b/lib/domain/customer/customer.dart @@ -0,0 +1,9 @@ +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../common/api/api_failure.dart'; + +part 'customer.freezed.dart'; +part 'entities/customer_entity.dart'; +part 'failures/customer_failure.dart'; +part 'repositories/i_customer_repository.dart'; diff --git a/lib/domain/customer/customer.freezed.dart b/lib/domain/customer/customer.freezed.dart new file mode 100644 index 0000000..d970276 --- /dev/null +++ b/lib/domain/customer/customer.freezed.dart @@ -0,0 +1,1015 @@ +// 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 'customer.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 _$Customer { + String get id => throw _privateConstructorUsedError; + String get organizationId => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get phone => throw _privateConstructorUsedError; + String get address => throw _privateConstructorUsedError; + bool get isDefault => throw _privateConstructorUsedError; + bool get isActive => throw _privateConstructorUsedError; + Map get metadata => throw _privateConstructorUsedError; + String get createdAt => throw _privateConstructorUsedError; + String get updatedAt => throw _privateConstructorUsedError; + + /// Create a copy of Customer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CustomerCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerCopyWith<$Res> { + factory $CustomerCopyWith(Customer value, $Res Function(Customer) then) = + _$CustomerCopyWithImpl<$Res, Customer>; + @useResult + $Res call({ + String id, + String organizationId, + String name, + String email, + String phone, + String address, + bool isDefault, + bool isActive, + Map metadata, + String createdAt, + String updatedAt, + }); +} + +/// @nodoc +class _$CustomerCopyWithImpl<$Res, $Val extends Customer> + implements $CustomerCopyWith<$Res> { + _$CustomerCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Customer + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? organizationId = null, + Object? name = null, + Object? email = null, + Object? phone = null, + Object? address = null, + Object? isDefault = null, + Object? isActive = null, + Object? metadata = null, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + organizationId: null == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + phone: null == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String, + address: null == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as String, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + metadata: null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CustomerImplCopyWith<$Res> + implements $CustomerCopyWith<$Res> { + factory _$$CustomerImplCopyWith( + _$CustomerImpl value, + $Res Function(_$CustomerImpl) then, + ) = __$$CustomerImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String organizationId, + String name, + String email, + String phone, + String address, + bool isDefault, + bool isActive, + Map metadata, + String createdAt, + String updatedAt, + }); +} + +/// @nodoc +class __$$CustomerImplCopyWithImpl<$Res> + extends _$CustomerCopyWithImpl<$Res, _$CustomerImpl> + implements _$$CustomerImplCopyWith<$Res> { + __$$CustomerImplCopyWithImpl( + _$CustomerImpl _value, + $Res Function(_$CustomerImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Customer + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? organizationId = null, + Object? name = null, + Object? email = null, + Object? phone = null, + Object? address = null, + Object? isDefault = null, + Object? isActive = null, + Object? metadata = null, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then( + _$CustomerImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + organizationId: null == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + phone: null == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String, + address: null == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as String, + isDefault: null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + metadata: null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$CustomerImpl implements _Customer { + const _$CustomerImpl({ + required this.id, + required this.organizationId, + required this.name, + required this.email, + required this.phone, + required this.address, + required this.isDefault, + required this.isActive, + required final Map metadata, + required this.createdAt, + required this.updatedAt, + }) : _metadata = metadata; + + @override + final String id; + @override + final String organizationId; + @override + final String name; + @override + final String email; + @override + final String phone; + @override + final String address; + @override + final bool isDefault; + @override + final bool isActive; + final Map _metadata; + @override + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + final String createdAt; + @override + final String updatedAt; + + @override + String toString() { + return 'Customer(id: $id, organizationId: $organizationId, name: $name, email: $email, phone: $phone, address: $address, isDefault: $isDefault, isActive: $isActive, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CustomerImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.organizationId, organizationId) || + other.organizationId == organizationId) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.phone, phone) || other.phone == phone) && + (identical(other.address, address) || other.address == address) && + (identical(other.isDefault, isDefault) || + other.isDefault == isDefault) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + const DeepCollectionEquality().equals(other._metadata, _metadata) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + id, + organizationId, + name, + email, + phone, + address, + isDefault, + isActive, + const DeepCollectionEquality().hash(_metadata), + createdAt, + updatedAt, + ); + + /// Create a copy of Customer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CustomerImplCopyWith<_$CustomerImpl> get copyWith => + __$$CustomerImplCopyWithImpl<_$CustomerImpl>(this, _$identity); +} + +abstract class _Customer implements Customer { + const factory _Customer({ + required final String id, + required final String organizationId, + required final String name, + required final String email, + required final String phone, + required final String address, + required final bool isDefault, + required final bool isActive, + required final Map metadata, + required final String createdAt, + required final String updatedAt, + }) = _$CustomerImpl; + + @override + String get id; + @override + String get organizationId; + @override + String get name; + @override + String get email; + @override + String get phone; + @override + String get address; + @override + bool get isDefault; + @override + bool get isActive; + @override + Map get metadata; + @override + String get createdAt; + @override + String get updatedAt; + + /// Create a copy of Customer + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CustomerImplCopyWith<_$CustomerImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$CustomerFailure { + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function() empty, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function()? empty, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function()? empty, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_Empty value) empty, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_Empty value)? empty, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_Empty value)? empty, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerFailureCopyWith<$Res> { + factory $CustomerFailureCopyWith( + CustomerFailure value, + $Res Function(CustomerFailure) then, + ) = _$CustomerFailureCopyWithImpl<$Res, CustomerFailure>; +} + +/// @nodoc +class _$CustomerFailureCopyWithImpl<$Res, $Val extends CustomerFailure> + implements $CustomerFailureCopyWith<$Res> { + _$CustomerFailureCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$ServerErrorImplCopyWith<$Res> { + factory _$$ServerErrorImplCopyWith( + _$ServerErrorImpl value, + $Res Function(_$ServerErrorImpl) then, + ) = __$$ServerErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({ApiFailure failure}); + + $ApiFailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$ServerErrorImplCopyWithImpl<$Res> + extends _$CustomerFailureCopyWithImpl<$Res, _$ServerErrorImpl> + implements _$$ServerErrorImplCopyWith<$Res> { + __$$ServerErrorImplCopyWithImpl( + _$ServerErrorImpl _value, + $Res Function(_$ServerErrorImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? failure = null}) { + return _then( + _$ServerErrorImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as ApiFailure, + ), + ); + } + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ApiFailureCopyWith<$Res> get failure { + return $ApiFailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$ServerErrorImpl implements _ServerError { + const _$ServerErrorImpl(this.failure); + + @override + final ApiFailure failure; + + @override + String toString() { + return 'CustomerFailure.serverError(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ServerErrorImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ServerErrorImplCopyWith<_$ServerErrorImpl> get copyWith => + __$$ServerErrorImplCopyWithImpl<_$ServerErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function() empty, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return serverError(failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function()? empty, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return serverError?.call(failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function()? empty, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (serverError != null) { + return serverError(failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_Empty value) empty, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return serverError(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_Empty value)? empty, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return serverError?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_Empty value)? empty, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (serverError != null) { + return serverError(this); + } + return orElse(); + } +} + +abstract class _ServerError implements CustomerFailure { + const factory _ServerError(final ApiFailure failure) = _$ServerErrorImpl; + + ApiFailure get failure; + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ServerErrorImplCopyWith<_$ServerErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$UnexpectedErrorImplCopyWith<$Res> { + factory _$$UnexpectedErrorImplCopyWith( + _$UnexpectedErrorImpl value, + $Res Function(_$UnexpectedErrorImpl) then, + ) = __$$UnexpectedErrorImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$UnexpectedErrorImplCopyWithImpl<$Res> + extends _$CustomerFailureCopyWithImpl<$Res, _$UnexpectedErrorImpl> + implements _$$UnexpectedErrorImplCopyWith<$Res> { + __$$UnexpectedErrorImplCopyWithImpl( + _$UnexpectedErrorImpl _value, + $Res Function(_$UnexpectedErrorImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$UnexpectedErrorImpl implements _UnexpectedError { + const _$UnexpectedErrorImpl(); + + @override + String toString() { + return 'CustomerFailure.unexpectedError()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$UnexpectedErrorImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function() empty, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return unexpectedError(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function()? empty, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return unexpectedError?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function()? empty, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (unexpectedError != null) { + return unexpectedError(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_Empty value) empty, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return unexpectedError(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_Empty value)? empty, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return unexpectedError?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_Empty value)? empty, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (unexpectedError != null) { + return unexpectedError(this); + } + return orElse(); + } +} + +abstract class _UnexpectedError implements CustomerFailure { + const factory _UnexpectedError() = _$UnexpectedErrorImpl; +} + +/// @nodoc +abstract class _$$EmptyImplCopyWith<$Res> { + factory _$$EmptyImplCopyWith( + _$EmptyImpl value, + $Res Function(_$EmptyImpl) then, + ) = __$$EmptyImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$EmptyImplCopyWithImpl<$Res> + extends _$CustomerFailureCopyWithImpl<$Res, _$EmptyImpl> + implements _$$EmptyImplCopyWith<$Res> { + __$$EmptyImplCopyWithImpl( + _$EmptyImpl _value, + $Res Function(_$EmptyImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$EmptyImpl implements _Empty { + const _$EmptyImpl(); + + @override + String toString() { + return 'CustomerFailure.empty()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$EmptyImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function() empty, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return empty(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function()? empty, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return empty?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function()? empty, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (empty != null) { + return empty(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_Empty value) empty, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return empty(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_Empty value)? empty, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return empty?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_Empty value)? empty, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (empty != null) { + return empty(this); + } + return orElse(); + } +} + +abstract class _Empty implements CustomerFailure { + const factory _Empty() = _$EmptyImpl; +} + +/// @nodoc +abstract class _$$DynamicErrorMessageImplCopyWith<$Res> { + factory _$$DynamicErrorMessageImplCopyWith( + _$DynamicErrorMessageImpl value, + $Res Function(_$DynamicErrorMessageImpl) then, + ) = __$$DynamicErrorMessageImplCopyWithImpl<$Res>; + @useResult + $Res call({String erroMessage}); +} + +/// @nodoc +class __$$DynamicErrorMessageImplCopyWithImpl<$Res> + extends _$CustomerFailureCopyWithImpl<$Res, _$DynamicErrorMessageImpl> + implements _$$DynamicErrorMessageImplCopyWith<$Res> { + __$$DynamicErrorMessageImplCopyWithImpl( + _$DynamicErrorMessageImpl _value, + $Res Function(_$DynamicErrorMessageImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? erroMessage = null}) { + return _then( + _$DynamicErrorMessageImpl( + null == erroMessage + ? _value.erroMessage + : erroMessage // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$DynamicErrorMessageImpl implements _DynamicErrorMessage { + const _$DynamicErrorMessageImpl(this.erroMessage); + + @override + final String erroMessage; + + @override + String toString() { + return 'CustomerFailure.dynamicErrorMessage(erroMessage: $erroMessage)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DynamicErrorMessageImpl && + (identical(other.erroMessage, erroMessage) || + other.erroMessage == erroMessage)); + } + + @override + int get hashCode => Object.hash(runtimeType, erroMessage); + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DynamicErrorMessageImplCopyWith<_$DynamicErrorMessageImpl> get copyWith => + __$$DynamicErrorMessageImplCopyWithImpl<_$DynamicErrorMessageImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function() empty, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return dynamicErrorMessage(erroMessage); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function()? empty, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return dynamicErrorMessage?.call(erroMessage); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function()? empty, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (dynamicErrorMessage != null) { + return dynamicErrorMessage(erroMessage); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_Empty value) empty, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return dynamicErrorMessage(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_Empty value)? empty, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return dynamicErrorMessage?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_Empty value)? empty, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (dynamicErrorMessage != null) { + return dynamicErrorMessage(this); + } + return orElse(); + } +} + +abstract class _DynamicErrorMessage implements CustomerFailure { + const factory _DynamicErrorMessage(final String erroMessage) = + _$DynamicErrorMessageImpl; + + String get erroMessage; + + /// Create a copy of CustomerFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DynamicErrorMessageImplCopyWith<_$DynamicErrorMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/domain/customer/entities/customer_entity.dart b/lib/domain/customer/entities/customer_entity.dart new file mode 100644 index 0000000..5d1632c --- /dev/null +++ b/lib/domain/customer/entities/customer_entity.dart @@ -0,0 +1,32 @@ +part of '../customer.dart'; + +@freezed +class Customer with _$Customer { + const factory Customer({ + required String id, + required String organizationId, + required String name, + required String email, + required String phone, + required String address, + required bool isDefault, + required bool isActive, + required Map metadata, + required String createdAt, + required String updatedAt, + }) = _Customer; + + factory Customer.empty() => Customer( + id: '', + organizationId: '', + name: '', + email: '', + phone: '', + address: '', + isDefault: false, + isActive: false, + metadata: const {}, + createdAt: '', + updatedAt: '', + ); +} diff --git a/lib/domain/customer/failures/customer_failure.dart b/lib/domain/customer/failures/customer_failure.dart new file mode 100644 index 0000000..48d0f86 --- /dev/null +++ b/lib/domain/customer/failures/customer_failure.dart @@ -0,0 +1,10 @@ +part of '../customer.dart'; + +@freezed +sealed class CustomerFailure with _$CustomerFailure { + const factory CustomerFailure.serverError(ApiFailure failure) = _ServerError; + const factory CustomerFailure.unexpectedError() = _UnexpectedError; + const factory CustomerFailure.empty() = _Empty; + const factory CustomerFailure.dynamicErrorMessage(String erroMessage) = + _DynamicErrorMessage; +} diff --git a/lib/domain/customer/repositories/i_customer_repository.dart b/lib/domain/customer/repositories/i_customer_repository.dart new file mode 100644 index 0000000..6efbac2 --- /dev/null +++ b/lib/domain/customer/repositories/i_customer_repository.dart @@ -0,0 +1,9 @@ +part of '../customer.dart'; + +abstract class ICustomerRepository { + Future>> get({ + int page = 1, + int limit = 20, + String? search, + }); +} diff --git a/lib/infrastructure/customer/customer_dtos.dart b/lib/infrastructure/customer/customer_dtos.dart new file mode 100644 index 0000000..ca0a1f0 --- /dev/null +++ b/lib/infrastructure/customer/customer_dtos.dart @@ -0,0 +1,8 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../domain/customer/customer.dart'; + +part 'customer_dtos.freezed.dart'; +part 'customer_dtos.g.dart'; + +part 'dto/customer_dto.dart'; diff --git a/lib/infrastructure/customer/customer_dtos.freezed.dart b/lib/infrastructure/customer/customer_dtos.freezed.dart new file mode 100644 index 0000000..1f3f52d --- /dev/null +++ b/lib/infrastructure/customer/customer_dtos.freezed.dart @@ -0,0 +1,440 @@ +// 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 'customer_dtos.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', +); + +CustomerDto _$CustomerDtoFromJson(Map json) { + return _CustomerDto.fromJson(json); +} + +/// @nodoc +mixin _$CustomerDto { + @JsonKey(name: "id") + String? get id => throw _privateConstructorUsedError; + @JsonKey(name: "organization_id") + String? get organizationId => throw _privateConstructorUsedError; + @JsonKey(name: "name") + String? get name => throw _privateConstructorUsedError; + @JsonKey(name: "email") + String? get email => throw _privateConstructorUsedError; + @JsonKey(name: "phone") + String? get phone => throw _privateConstructorUsedError; + @JsonKey(name: "address") + String? get address => throw _privateConstructorUsedError; + @JsonKey(name: "is_default") + bool? get isDefault => throw _privateConstructorUsedError; + @JsonKey(name: "is_active") + bool? get isActive => throw _privateConstructorUsedError; + @JsonKey(name: "metadata") + Map? get metadata => throw _privateConstructorUsedError; + @JsonKey(name: "created_at") + String? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: "updated_at") + String? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this CustomerDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CustomerDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CustomerDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerDtoCopyWith<$Res> { + factory $CustomerDtoCopyWith( + CustomerDto value, + $Res Function(CustomerDto) then, + ) = _$CustomerDtoCopyWithImpl<$Res, CustomerDto>; + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "name") String? name, + @JsonKey(name: "email") String? email, + @JsonKey(name: "phone") String? phone, + @JsonKey(name: "address") String? address, + @JsonKey(name: "is_default") bool? isDefault, + @JsonKey(name: "is_active") bool? isActive, + @JsonKey(name: "metadata") Map? metadata, + @JsonKey(name: "created_at") String? createdAt, + @JsonKey(name: "updated_at") String? updatedAt, + }); +} + +/// @nodoc +class _$CustomerDtoCopyWithImpl<$Res, $Val extends CustomerDto> + implements $CustomerDtoCopyWith<$Res> { + _$CustomerDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CustomerDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? organizationId = freezed, + Object? name = freezed, + Object? email = freezed, + Object? phone = freezed, + Object? address = freezed, + Object? isDefault = freezed, + Object? isActive = freezed, + Object? metadata = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + organizationId: freezed == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + phone: freezed == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String?, + address: freezed == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as String?, + isDefault: freezed == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CustomerDtoImplCopyWith<$Res> + implements $CustomerDtoCopyWith<$Res> { + factory _$$CustomerDtoImplCopyWith( + _$CustomerDtoImpl value, + $Res Function(_$CustomerDtoImpl) then, + ) = __$$CustomerDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "name") String? name, + @JsonKey(name: "email") String? email, + @JsonKey(name: "phone") String? phone, + @JsonKey(name: "address") String? address, + @JsonKey(name: "is_default") bool? isDefault, + @JsonKey(name: "is_active") bool? isActive, + @JsonKey(name: "metadata") Map? metadata, + @JsonKey(name: "created_at") String? createdAt, + @JsonKey(name: "updated_at") String? updatedAt, + }); +} + +/// @nodoc +class __$$CustomerDtoImplCopyWithImpl<$Res> + extends _$CustomerDtoCopyWithImpl<$Res, _$CustomerDtoImpl> + implements _$$CustomerDtoImplCopyWith<$Res> { + __$$CustomerDtoImplCopyWithImpl( + _$CustomerDtoImpl _value, + $Res Function(_$CustomerDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CustomerDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? organizationId = freezed, + Object? name = freezed, + Object? email = freezed, + Object? phone = freezed, + Object? address = freezed, + Object? isDefault = freezed, + Object? isActive = freezed, + Object? metadata = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$CustomerDtoImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + organizationId: freezed == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + phone: freezed == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String?, + address: freezed == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as String?, + isDefault: freezed == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + metadata: freezed == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CustomerDtoImpl extends _CustomerDto { + const _$CustomerDtoImpl({ + @JsonKey(name: "id") this.id, + @JsonKey(name: "organization_id") this.organizationId, + @JsonKey(name: "name") this.name, + @JsonKey(name: "email") this.email, + @JsonKey(name: "phone") this.phone, + @JsonKey(name: "address") this.address, + @JsonKey(name: "is_default") this.isDefault, + @JsonKey(name: "is_active") this.isActive, + @JsonKey(name: "metadata") final Map? metadata, + @JsonKey(name: "created_at") this.createdAt, + @JsonKey(name: "updated_at") this.updatedAt, + }) : _metadata = metadata, + super._(); + + factory _$CustomerDtoImpl.fromJson(Map json) => + _$$CustomerDtoImplFromJson(json); + + @override + @JsonKey(name: "id") + final String? id; + @override + @JsonKey(name: "organization_id") + final String? organizationId; + @override + @JsonKey(name: "name") + final String? name; + @override + @JsonKey(name: "email") + final String? email; + @override + @JsonKey(name: "phone") + final String? phone; + @override + @JsonKey(name: "address") + final String? address; + @override + @JsonKey(name: "is_default") + final bool? isDefault; + @override + @JsonKey(name: "is_active") + final bool? isActive; + final Map? _metadata; + @override + @JsonKey(name: "metadata") + Map? get metadata { + final value = _metadata; + if (value == null) return null; + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + @JsonKey(name: "created_at") + final String? createdAt; + @override + @JsonKey(name: "updated_at") + final String? updatedAt; + + @override + String toString() { + return 'CustomerDto(id: $id, organizationId: $organizationId, name: $name, email: $email, phone: $phone, address: $address, isDefault: $isDefault, isActive: $isActive, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CustomerDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.organizationId, organizationId) || + other.organizationId == organizationId) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.phone, phone) || other.phone == phone) && + (identical(other.address, address) || other.address == address) && + (identical(other.isDefault, isDefault) || + other.isDefault == isDefault) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + const DeepCollectionEquality().equals(other._metadata, _metadata) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + organizationId, + name, + email, + phone, + address, + isDefault, + isActive, + const DeepCollectionEquality().hash(_metadata), + createdAt, + updatedAt, + ); + + /// Create a copy of CustomerDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CustomerDtoImplCopyWith<_$CustomerDtoImpl> get copyWith => + __$$CustomerDtoImplCopyWithImpl<_$CustomerDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$CustomerDtoImplToJson(this); + } +} + +abstract class _CustomerDto extends CustomerDto { + const factory _CustomerDto({ + @JsonKey(name: "id") final String? id, + @JsonKey(name: "organization_id") final String? organizationId, + @JsonKey(name: "name") final String? name, + @JsonKey(name: "email") final String? email, + @JsonKey(name: "phone") final String? phone, + @JsonKey(name: "address") final String? address, + @JsonKey(name: "is_default") final bool? isDefault, + @JsonKey(name: "is_active") final bool? isActive, + @JsonKey(name: "metadata") final Map? metadata, + @JsonKey(name: "created_at") final String? createdAt, + @JsonKey(name: "updated_at") final String? updatedAt, + }) = _$CustomerDtoImpl; + const _CustomerDto._() : super._(); + + factory _CustomerDto.fromJson(Map json) = + _$CustomerDtoImpl.fromJson; + + @override + @JsonKey(name: "id") + String? get id; + @override + @JsonKey(name: "organization_id") + String? get organizationId; + @override + @JsonKey(name: "name") + String? get name; + @override + @JsonKey(name: "email") + String? get email; + @override + @JsonKey(name: "phone") + String? get phone; + @override + @JsonKey(name: "address") + String? get address; + @override + @JsonKey(name: "is_default") + bool? get isDefault; + @override + @JsonKey(name: "is_active") + bool? get isActive; + @override + @JsonKey(name: "metadata") + Map? get metadata; + @override + @JsonKey(name: "created_at") + String? get createdAt; + @override + @JsonKey(name: "updated_at") + String? get updatedAt; + + /// Create a copy of CustomerDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CustomerDtoImplCopyWith<_$CustomerDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/infrastructure/customer/customer_dtos.g.dart b/lib/infrastructure/customer/customer_dtos.g.dart new file mode 100644 index 0000000..148d005 --- /dev/null +++ b/lib/infrastructure/customer/customer_dtos.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'customer_dtos.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$CustomerDtoImpl _$$CustomerDtoImplFromJson(Map json) => + _$CustomerDtoImpl( + id: json['id'] as String?, + organizationId: json['organization_id'] as String?, + name: json['name'] as String?, + email: json['email'] as String?, + phone: json['phone'] as String?, + address: json['address'] as String?, + isDefault: json['is_default'] as bool?, + isActive: json['is_active'] as bool?, + metadata: json['metadata'] as Map?, + createdAt: json['created_at'] as String?, + updatedAt: json['updated_at'] as String?, + ); + +Map _$$CustomerDtoImplToJson(_$CustomerDtoImpl instance) => + { + 'id': instance.id, + 'organization_id': instance.organizationId, + 'name': instance.name, + 'email': instance.email, + 'phone': instance.phone, + 'address': instance.address, + 'is_default': instance.isDefault, + 'is_active': instance.isActive, + 'metadata': instance.metadata, + 'created_at': instance.createdAt, + 'updated_at': instance.updatedAt, + }; diff --git a/lib/infrastructure/customer/datasources/remote_data_provider.dart b/lib/infrastructure/customer/datasources/remote_data_provider.dart new file mode 100644 index 0000000..442a093 --- /dev/null +++ b/lib/infrastructure/customer/datasources/remote_data_provider.dart @@ -0,0 +1,52 @@ +import 'dart:developer'; + +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/customer/customer.dart'; +import '../customer_dtos.dart'; + +@injectable +class CustomerRemoteDataProvider { + final ApiClient _apiClient; + final String _logName = "CustomerRemoteDataProvider"; + + CustomerRemoteDataProvider(this._apiClient); + + Future>> fetch({ + int page = 1, + int limit = 20, + String? search, + }) async { + try { + Map params = {'page': page, 'limit': limit}; + + if (search != null && search.isNotEmpty) { + params['search'] = search; + } + + final response = await _apiClient.get( + ApiPath.customer, + params: params, + headers: getAuthorizationHeader(), + ); + + if (response.data['data'] == null) { + return DC.error(CustomerFailure.empty()); + } + + final dto = (response.data['data']['data'] as List) + .map((e) => CustomerDto.fromJson(e)) + .toList(); + + return DC.data(dto); + } on ApiFailure catch (e, s) { + log('fetchCustomerError', name: _logName, error: e, stackTrace: s); + return DC.error(CustomerFailure.serverError(e)); + } + } +} diff --git a/lib/infrastructure/customer/dto/customer_dto.dart b/lib/infrastructure/customer/dto/customer_dto.dart new file mode 100644 index 0000000..46cba2a --- /dev/null +++ b/lib/infrastructure/customer/dto/customer_dto.dart @@ -0,0 +1,39 @@ +part of '../customer_dtos.dart'; + +@freezed +class CustomerDto with _$CustomerDto { + const CustomerDto._(); + + const factory CustomerDto({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "name") String? name, + @JsonKey(name: "email") String? email, + @JsonKey(name: "phone") String? phone, + @JsonKey(name: "address") String? address, + @JsonKey(name: "is_default") bool? isDefault, + @JsonKey(name: "is_active") bool? isActive, + @JsonKey(name: "metadata") Map? metadata, + @JsonKey(name: "created_at") String? createdAt, + @JsonKey(name: "updated_at") String? updatedAt, + }) = _CustomerDto; + + factory CustomerDto.fromJson(Map json) => + _$CustomerDtoFromJson(json); + + Customer toDomain() { + return Customer( + id: id ?? '', + organizationId: organizationId ?? '', + name: name ?? '', + email: email ?? '', + phone: phone ?? '', + address: address ?? '', + isDefault: isDefault ?? false, + isActive: isActive ?? false, + metadata: metadata ?? {}, + createdAt: createdAt ?? '', + updatedAt: updatedAt ?? '', + ); + } +} diff --git a/lib/infrastructure/customer/repositories/customer_repository.dart b/lib/infrastructure/customer/repositories/customer_repository.dart new file mode 100644 index 0000000..b826b8d --- /dev/null +++ b/lib/infrastructure/customer/repositories/customer_repository.dart @@ -0,0 +1,41 @@ +import 'dart:developer'; + +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/customer/customer.dart'; +import '../datasources/remote_data_provider.dart'; + +@Injectable(as: ICustomerRepository) +class CustomerRepository implements ICustomerRepository { + final CustomerRemoteDataProvider _dataProvider; + final String _logName = 'CustomerRepository'; + + CustomerRepository(this._dataProvider); + + @override + Future>> get({ + int page = 1, + int limit = 20, + String? search, + }) async { + try { + final result = await _dataProvider.fetch( + page: page, + limit: limit, + search: search, + ); + + if (result.hasError) { + return left(result.error!); + } + + final auth = result.data!.map((item) => item.toDomain()).toList(); + + return right(auth); + } catch (e, s) { + log('getCustomerError', name: _logName, error: e, stackTrace: s); + return left(const CustomerFailure.unexpectedError()); + } + } +} diff --git a/lib/injection.config.dart b/lib/injection.config.dart index e51896f..2a46271 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -24,6 +24,8 @@ import 'package:apskel_owner_flutter/application/auth/logout_form/logout_form_bl as _i574; import 'package:apskel_owner_flutter/application/category/category_loader/category_loader_bloc.dart' as _i183; +import 'package:apskel_owner_flutter/application/customer/customer_loader/customer_loader_bloc.dart' + as _i972; import 'package:apskel_owner_flutter/application/language/language_bloc.dart' as _i455; import 'package:apskel_owner_flutter/application/product/product_loader/product_loader_bloc.dart' @@ -41,6 +43,7 @@ import 'package:apskel_owner_flutter/domain/analytic/repositories/i_analytic_rep as _i477; import 'package:apskel_owner_flutter/domain/auth/auth.dart' as _i49; import 'package:apskel_owner_flutter/domain/category/category.dart' as _i1020; +import 'package:apskel_owner_flutter/domain/customer/customer.dart' as _i48; import 'package:apskel_owner_flutter/domain/product/product.dart' as _i419; import 'package:apskel_owner_flutter/env.dart' as _i6; import 'package:apskel_owner_flutter/infrastructure/analytic/datasource/remote_data_provider.dart' @@ -57,6 +60,10 @@ import 'package:apskel_owner_flutter/infrastructure/category/datasource/remote_d as _i333; import 'package:apskel_owner_flutter/infrastructure/category/repositories/category_repository.dart' as _i869; +import 'package:apskel_owner_flutter/infrastructure/customer/datasources/remote_data_provider.dart' + as _i1006; +import 'package:apskel_owner_flutter/infrastructure/customer/repositories/customer_repository.dart' + as _i550; import 'package:apskel_owner_flutter/infrastructure/product/datasources/remote_data_provider.dart' as _i823; import 'package:apskel_owner_flutter/infrastructure/product/repositories/product_repository.dart' @@ -122,6 +129,12 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i866.AnalyticRemoteDataProvider>( () => _i866.AnalyticRemoteDataProvider(gh<_i115.ApiClient>()), ); + gh.factory<_i1006.CustomerRemoteDataProvider>( + () => _i1006.CustomerRemoteDataProvider(gh<_i115.ApiClient>()), + ); + gh.factory<_i48.ICustomerRepository>( + () => _i550.CustomerRepository(gh<_i1006.CustomerRemoteDataProvider>()), + ); gh.factory<_i49.IAuthRepository>( () => _i1035.AuthRepository( gh<_i991.AuthLocalDataProvider>(), @@ -131,6 +144,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i419.IProductRepository>( () => _i121.ProductRepository(gh<_i823.ProductRemoteDataProvider>()), ); + gh.factory<_i972.CustomerLoaderBloc>( + () => _i972.CustomerLoaderBloc(gh<_i48.ICustomerRepository>()), + ); gh.factory<_i477.IAnalyticRepository>( () => _i393.AnalyticRepository( gh<_i866.AnalyticRemoteDataProvider>(), diff --git a/lib/presentation/pages/customer/customer_page.dart b/lib/presentation/pages/customer/customer_page.dart index da89db4..9b63756 100644 --- a/lib/presentation/pages/customer/customer_page.dart +++ b/lib/presentation/pages/customer/customer_page.dart @@ -1,106 +1,39 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; +import '../../../application/customer/customer_loader/customer_loader_bloc.dart'; import '../../../common/theme/theme.dart'; +import '../../../domain/customer/customer.dart'; +import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import '../../components/button/button.dart'; import 'widgets/customer_card.dart'; import 'widgets/customer_tile.dart'; -// Customer Model -class Customer { - final String id; - final String name; - final String email; - final String phone; - final String address; - final double totalPurchases; - final int totalOrders; - final DateTime lastVisit; - final String membershipLevel; - final bool isActive; - - Customer({ - required this.id, - required this.name, - required this.email, - required this.phone, - required this.address, - required this.totalPurchases, - required this.totalOrders, - required this.lastVisit, - required this.membershipLevel, - required this.isActive, - }); -} - @RoutePage() -class CustomerPage extends StatefulWidget { +class CustomerPage extends StatefulWidget implements AutoRouteWrapper { const CustomerPage({super.key}); @override State createState() => _CustomerPageState(); + + @override + Widget wrappedRoute(BuildContext context) => BlocProvider( + create: (context) => + getIt() + ..add(CustomerLoaderEvent.fetched(isRefresh: true)), + child: this, + ); } class _CustomerPageState extends State with TickerProviderStateMixin { final TextEditingController _searchController = TextEditingController(); - String _searchQuery = ''; + ScrollController _scrollController = ScrollController(); bool _isGridView = false; - // Sample customer data - final List _customers = [ - Customer( - id: '001', - name: 'Ahmad Wijaya', - email: 'ahmad@email.com', - phone: '+62 812-3456-7890', - address: 'Jl. Raya No. 123, Jakarta', - totalPurchases: 2500000, - totalOrders: 15, - lastVisit: DateTime.now().subtract(Duration(days: 2)), - membershipLevel: 'Gold', - isActive: true, - ), - Customer( - id: '002', - name: 'Siti Nurhaliza', - email: 'siti@email.com', - phone: '+62 813-4567-8901', - address: 'Jl. Merdeka No. 45, Bandung', - totalPurchases: 1800000, - totalOrders: 12, - lastVisit: DateTime.now().subtract(Duration(days: 5)), - membershipLevel: 'Silver', - isActive: true, - ), - Customer( - id: '003', - name: 'Budi Santoso', - email: 'budi@email.com', - phone: '+62 814-5678-9012', - address: 'Jl. Sudirman No. 67, Surabaya', - totalPurchases: 3200000, - totalOrders: 20, - lastVisit: DateTime.now().subtract(Duration(days: 1)), - membershipLevel: 'Platinum', - isActive: true, - ), - Customer( - id: '004', - name: 'Maya Sari', - email: 'maya@email.com', - phone: '+62 815-6789-0123', - address: 'Jl. Diponegoro No. 89, Yogyakarta', - totalPurchases: 950000, - totalOrders: 8, - lastVisit: DateTime.now().subtract(Duration(days: 30)), - membershipLevel: 'Bronze', - isActive: false, - ), - ]; - @override initState() { super.initState(); @@ -112,94 +45,102 @@ class _CustomerPageState extends State super.dispose(); } - List get filteredCustomers { - var filtered = _customers.where((customer) { - final matchesSearch = - customer.name.toLowerCase().contains(_searchQuery.toLowerCase()) || - customer.email.toLowerCase().contains(_searchQuery.toLowerCase()) || - customer.phone.contains(_searchQuery); - - return matchesSearch; - }).toList(); - - return filtered; - } - @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, - body: CustomScrollView( - slivers: [ - // SliverAppBar with gradient - SliverAppBar( - expandedHeight: 120.0, - floating: false, - pinned: true, - backgroundColor: AppColor.primary, - flexibleSpace: CustomAppBar(title: 'Pelanggan'), - actions: [ActionIconButton(onTap: () {}, icon: LineIcons.search)], - ), + body: BlocBuilder( + builder: (context, state) { + return NotificationListener( + onNotification: (notification) { + if (notification is ScrollEndNotification && + _scrollController.position.extentAfter == 0) { + context.read().add( + CustomerLoaderEvent.fetched(), + ); + return true; + } - // Search and Filter Section - SliverToBoxAdapter( - child: Container( - color: AppColor.white, - child: Column( - children: [ - // View toggle and sort - Padding( - padding: EdgeInsets.only(left: 16, right: 16, bottom: 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return true; + }, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + // SliverAppBar with gradient + SliverAppBar( + expandedHeight: 120.0, + floating: false, + pinned: true, + backgroundColor: AppColor.primary, + flexibleSpace: CustomAppBar(title: 'Pelanggan'), + actions: [ + ActionIconButton(onTap: () {}, icon: LineIcons.search), + ], + ), + + // Search and Filter Section + SliverToBoxAdapter( + child: Container( + color: AppColor.white, + child: Column( children: [ - Text( - '${filteredCustomers.length} customers found', - style: TextStyle( - color: AppColor.textSecondary, - fontSize: 14, + // View toggle and sort + Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + bottom: 0, ), - ), - Row( - children: [ - IconButton( - icon: Icon( - _isGridView ? Icons.list : Icons.grid_view, - color: AppColor.primary, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + IconButton( + icon: Icon( + _isGridView + ? Icons.list + : Icons.grid_view, + color: AppColor.primary, + ), + onPressed: () { + setState(() { + _isGridView = !_isGridView; + }); + }, + ), + ], ), - onPressed: () { - setState(() { - _isGridView = !_isGridView; - }); - }, - ), - ], + ], + ), ), ], ), ), - ], - ), - ), - ), + ), - // Customer List - _isGridView ? _buildCustomerGrid() : _buildCustomerList(), - ], + // Customer List + _isGridView + ? _buildCustomerGrid(state.customers) + : _buildCustomerList(state.customers), + ], + ), + ); + }, ), ); } - Widget _buildCustomerList() { + Widget _buildCustomerList(List customers) { return SliverList( delegate: SliverChildBuilderDelegate((context, index) { - final customer = filteredCustomers[index]; + final customer = customers[index]; return CustomerTile(customer: customer); - }, childCount: filteredCustomers.length), + }, childCount: customers.length), ); } - Widget _buildCustomerGrid() { + Widget _buildCustomerGrid(List customers) { return SliverPadding( padding: EdgeInsets.all(16), sliver: SliverGrid( @@ -210,9 +151,9 @@ class _CustomerPageState extends State childAspectRatio: 0.8, ), delegate: SliverChildBuilderDelegate((context, index) { - final customer = filteredCustomers[index]; + final customer = customers[index]; return CustomerCard(customer: customer); - }, childCount: filteredCustomers.length), + }, childCount: customers.length), ), ); } diff --git a/lib/presentation/pages/customer/widgets/customer_card.dart b/lib/presentation/pages/customer/widgets/customer_card.dart index 457c67f..e11fe96 100644 --- a/lib/presentation/pages/customer/widgets/customer_card.dart +++ b/lib/presentation/pages/customer/widgets/customer_card.dart @@ -1,89 +1,390 @@ import 'package:flutter/material.dart'; - import '../../../../common/theme/theme.dart'; +import '../../../../domain/customer/customer.dart'; import '../../../components/spacer/spacer.dart'; -import '../customer_page.dart'; class CustomerCard extends StatelessWidget { final Customer customer; - const CustomerCard({super.key, required this.customer}); + final VoidCallback? onTap; + final VoidCallback? onLongPress; + + const CustomerCard({ + super.key, + required this.customer, + this.onTap, + this.onLongPress, + }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColor.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 4), + color: Colors.black.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 6), + spreadRadius: -4, ), ], + border: Border.all( + color: customer.isActive + ? AppColor.primary.withOpacity(0.1) + : Colors.grey.withOpacity(0.08), + width: 1.5, + ), ), - child: InkWell( - onTap: () {}, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - children: [ - CircleAvatar( - backgroundColor: _getMembershipColor(customer.membershipLevel), - radius: 30, - child: Text( - customer.name[0].toUpperCase(), - style: AppStyle.xxl.copyWith( - color: AppColor.white, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + onLongPress: onLongPress, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Avatar with status indicator + Stack( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + _getAvatarColor(customer.name), + _getAvatarColor(customer.name).withOpacity(0.8), + ], + ), + boxShadow: [ + BoxShadow( + color: _getAvatarColor( + customer.name, + ).withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 32, + child: Text( + customer.name.isNotEmpty + ? customer.name[0].toUpperCase() + : '?', + style: AppStyle.xxl.copyWith( + color: AppColor.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + // Status indicator + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: customer.isActive + ? AppColor.success + : AppColor.error, + shape: BoxShape.circle, + border: Border.all(color: AppColor.white, width: 3), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + + // Default badge + if (customer.isDefault) + Positioned( + top: -2, + left: -8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppColor.primary, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: AppColor.primary.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + '⭐', + style: AppStyle.xs.copyWith( + color: AppColor.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + + const SpaceHeight(16), + + // Customer Name + Text( + customer.name.isNotEmpty ? customer.name : 'Unknown Customer', + style: AppStyle.lg.copyWith( fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SpaceHeight(8), + + // Contact Info + if (customer.email.isNotEmpty || customer.phone.isNotEmpty) ...[ + Column( + children: [ + if (customer.email.isNotEmpty) + _buildContactInfo(Icons.email_outlined, customer.email), + if (customer.email.isNotEmpty && + customer.phone.isNotEmpty) + const SpaceHeight(4), + if (customer.phone.isNotEmpty) + _buildContactInfo(Icons.phone_outlined, customer.phone), + ], + ), + const SpaceHeight(12), + ], + + // Status Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: customer.isActive + ? AppColor.success.withOpacity(0.1) + : AppColor.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: customer.isActive + ? AppColor.success.withOpacity(0.3) + : AppColor.error.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: customer.isActive + ? AppColor.success + : AppColor.error, + shape: BoxShape.circle, + ), + ), + const SpaceWidth(6), + Text( + customer.isActive ? 'Active' : 'Inactive', + style: AppStyle.sm.copyWith( + color: customer.isActive + ? AppColor.success + : AppColor.error, + fontWeight: FontWeight.w600, + ), + ), + ], ), ), - ), - SpaceHeight(12), - Text( - customer.name, - style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 8), - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: _getMembershipColor( - customer.membershipLevel, - ).withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - customer.membershipLevel, - style: AppStyle.sm.copyWith( - color: _getMembershipColor(customer.membershipLevel), - fontWeight: FontWeight.bold, + + // Additional info if available + if (customer.address.isNotEmpty) ...[ + const SpaceHeight(8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.location_on_outlined, + size: 14, + color: AppColor.textSecondary, + ), + const SpaceWidth(4), + Flexible( + child: Text( + customer.address, + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - ), - ), - ], + ], + + // Metadata info + if (customer.metadata.isNotEmpty && _hasRelevantMetadata()) ...[ + const SpaceHeight(8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.star_outline, + size: 12, + color: AppColor.primary, + ), + const SpaceWidth(4), + Text( + _getMetadataInfo(), + style: AppStyle.xs.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + + // Join date + if (customer.createdAt.isNotEmpty) ...[ + const SpaceHeight(8), + Text( + 'Joined ${_formatDate(customer.createdAt)}', + style: AppStyle.xs.copyWith(color: AppColor.textSecondary), + textAlign: TextAlign.center, + ), + ], + ], + ), ), ), ), ); } - Color _getMembershipColor(String level) { - switch (level) { - case 'Platinum': - return Color(0xFF9C27B0); - case 'Gold': - return Color(0xFFFF9800); - case 'Silver': - return Color(0xFF607D8B); - case 'Bronze': - return Color(0xFF795548); - default: - return AppColor.primary; + Widget _buildContactInfo(IconData icon, String text) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: AppColor.textSecondary), + const SpaceWidth(6), + Flexible( + child: Text( + text, + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + Color _getAvatarColor(String name) { + final colors = [ + AppColor.primary, + const Color(0xFF9C27B0), // Purple + const Color(0xFFFF9800), // Orange + const Color(0xFF607D8B), // Blue Grey + const Color(0xFF795548), // Brown + const Color(0xFF4CAF50), // Green + const Color(0xFF2196F3), // Blue + const Color(0xFFE91E63), // Pink + const Color(0xFF00BCD4), // Cyan + const Color(0xFFFF5722), // Deep Orange + ]; + + if (name.isEmpty) return AppColor.primary; + final index = name.hashCode.abs() % colors.length; + return colors[index]; + } + + String _formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + final now = DateTime.now(); + final difference = now.difference(date).inDays; + + if (difference == 0) { + return 'today'; + } else if (difference == 1) { + return 'yesterday'; + } else if (difference < 30) { + return '${difference}d ago'; + } else if (difference < 365) { + final months = (difference / 30).floor(); + return '${months}mo ago'; + } else { + final years = (difference / 365).floor(); + return '${years}y ago'; + } + } catch (e) { + return dateStr; } } + + bool _hasRelevantMetadata() { + return customer.metadata.containsKey('notes') || + customer.metadata.containsKey('tags') || + customer.metadata.containsKey('source') || + customer.metadata.containsKey('preferences') || + customer.metadata.containsKey('vip') || + customer.metadata.containsKey('tier'); + } + + String _getMetadataInfo() { + if (customer.metadata.containsKey('vip') && + customer.metadata['vip'] == true) { + return 'VIP'; + } + if (customer.metadata.containsKey('tier')) { + return customer.metadata['tier'].toString(); + } + if (customer.metadata.containsKey('tags')) { + final tags = customer.metadata['tags']; + if (tags is List && tags.isNotEmpty) { + return tags.first.toString(); + } + } + if (customer.metadata.containsKey('source')) { + return customer.metadata['source'].toString(); + } + return 'Special'; + } } diff --git a/lib/presentation/pages/customer/widgets/customer_tile.dart b/lib/presentation/pages/customer/widgets/customer_tile.dart index 69bf48a..414967e 100644 --- a/lib/presentation/pages/customer/widgets/customer_tile.dart +++ b/lib/presentation/pages/customer/widgets/customer_tile.dart @@ -1,127 +1,378 @@ import 'package:flutter/material.dart'; - import '../../../../common/theme/theme.dart'; +import '../../../../domain/customer/customer.dart'; import '../../../components/spacer/spacer.dart'; -import '../customer_page.dart'; class CustomerTile extends StatelessWidget { final Customer customer; - const CustomerTile({super.key, required this.customer}); + final VoidCallback? onTap; + final VoidCallback? onEdit; + final VoidCallback? onDelete; + + const CustomerTile({ + super.key, + required this.customer, + this.onTap, + this.onEdit, + this.onDelete, + }); @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.symmetric(horizontal: AppValue.margin, vertical: 6), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: AppColor.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 2), + color: Colors.black.withOpacity(0.06), + blurRadius: 16, + offset: const Offset(0, 4), + spreadRadius: -2, ), ], + border: Border.all( + color: customer.isActive + ? AppColor.primary.withOpacity(0.15) + : Colors.grey.withOpacity(0.1), + width: 1, + ), ), - child: ListTile( - contentPadding: EdgeInsets.all(16), - leading: CircleAvatar( - backgroundColor: _getMembershipColor(customer.membershipLevel), - child: Text( - customer.name[0].toUpperCase(), - style: AppStyle.sm.copyWith( - color: AppColor.white, - fontWeight: FontWeight.bold, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row + Row( + children: [ + // Avatar + Stack( + children: [ + CircleAvatar( + radius: 26, + backgroundColor: _getAvatarColor(customer.name), + child: Text( + customer.name.isNotEmpty + ? customer.name[0].toUpperCase() + : '?', + style: AppStyle.lg.copyWith( + color: AppColor.white, + fontWeight: FontWeight.bold, + ), + ), + ), + // Status indicator + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: customer.isActive + ? AppColor.success + : AppColor.error, + shape: BoxShape.circle, + border: Border.all( + color: AppColor.white, + width: 2, + ), + ), + ), + ), + ], + ), + const SpaceWidth(16), + + // Customer Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name with badges + Row( + children: [ + Expanded( + child: Text( + customer.name.isNotEmpty + ? customer.name + : 'Unknown Customer', + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (customer.isDefault) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'DEFAULT', + style: AppStyle.xs.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ], + ), + const SpaceHeight(4), + + // Contact info + if (customer.email.isNotEmpty) ...[ + Row( + children: [ + Icon( + Icons.email_outlined, + size: 16, + color: AppColor.textSecondary, + ), + const SpaceWidth(6), + Expanded( + child: Text( + customer.email, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SpaceHeight(4), + ], + + if (customer.phone.isNotEmpty) + Row( + children: [ + Icon( + Icons.phone_outlined, + size: 16, + color: AppColor.textSecondary, + ), + const SpaceWidth(6), + Text( + customer.phone, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + ), + ), + ], + ), + ], + ), + ), + ], + ), + + // Address section + if (customer.address.isNotEmpty) ...[ + const SpaceHeight(16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.background, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + Icons.location_on_outlined, + size: 18, + color: AppColor.textSecondary, + ), + const SpaceWidth(8), + Expanded( + child: Text( + customer.address, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + + // Footer with status and dates + const SpaceHeight(16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Status badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: customer.isActive + ? AppColor.success.withOpacity(0.1) + : AppColor.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: customer.isActive + ? AppColor.success + : AppColor.error, + shape: BoxShape.circle, + ), + ), + const SpaceWidth(6), + Text( + customer.isActive ? 'Active' : 'Inactive', + style: AppStyle.sm.copyWith( + color: customer.isActive + ? AppColor.success + : AppColor.error, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + + // Created date + if (customer.createdAt.isNotEmpty) + Text( + 'Joined ${_formatDate(customer.createdAt)}', + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + ), + ), + ], + ), + + // Metadata section (if has any relevant data) + if (customer.metadata.isNotEmpty && _hasRelevantMetadata()) ...[ + const SpaceHeight(12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColor.primary.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: AppColor.primary, + ), + const SpaceWidth(8), + Expanded( + child: Text( + _getMetadataInfo(), + style: AppStyle.xs.copyWith( + color: AppColor.primary, + ), + ), + ), + ], + ), + ), + ], + ], ), ), ), - title: Text( - customer.name, - style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpaceHeight(4), - Text(customer.email), - SpaceHeight(2), - Text(customer.phone), - SpaceHeight(4), - Row( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: _getMembershipColor( - customer.membershipLevel, - ).withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - customer.membershipLevel, - style: AppStyle.sm.copyWith( - color: _getMembershipColor(customer.membershipLevel), - fontWeight: FontWeight.bold, - ), - ), - ), - SpaceWidth(8), - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: customer.isActive - ? AppColor.success.withOpacity(0.1) - : AppColor.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - customer.isActive ? 'Active' : 'Inactive', - style: AppStyle.sm.copyWith( - color: customer.isActive - ? AppColor.success - : AppColor.error, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ], - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - 'Rp ${customer.totalPurchases.toStringAsFixed(0)}', - style: AppStyle.md.copyWith( - fontWeight: FontWeight.bold, - color: AppColor.primary, - ), - ), - Text( - '${customer.totalOrders} orders', - style: AppStyle.sm.copyWith(color: AppColor.textSecondary), - ), - ], - ), - onTap: () {}, ), ); } - Color _getMembershipColor(String level) { - switch (level) { - case 'Platinum': - return Color(0xFF9C27B0); - case 'Gold': - return Color(0xFFFF9800); - case 'Silver': - return Color(0xFF607D8B); - case 'Bronze': - return Color(0xFF795548); - default: - return AppColor.primary; + Color _getAvatarColor(String name) { + final colors = [ + AppColor.primary, + const Color(0xFF9C27B0), + const Color(0xFFFF9800), + const Color(0xFF607D8B), + const Color(0xFF795548), + const Color(0xFF4CAF50), + const Color(0xFF2196F3), + const Color(0xFFE91E63), + ]; + + final index = name.hashCode.abs() % colors.length; + return colors[index]; + } + + String _formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + final now = DateTime.now(); + final difference = now.difference(date).inDays; + + if (difference == 0) { + return 'today'; + } else if (difference == 1) { + return 'yesterday'; + } else if (difference < 30) { + return '${difference}d ago'; + } else if (difference < 365) { + final months = (difference / 30).floor(); + return '${months}mo ago'; + } else { + final years = (difference / 365).floor(); + return '${years}y ago'; + } + } catch (e) { + return dateStr; } } + + bool _hasRelevantMetadata() { + return customer.metadata.containsKey('notes') || + customer.metadata.containsKey('tags') || + customer.metadata.containsKey('source') || + customer.metadata.containsKey('preferences'); + } + + String _getMetadataInfo() { + final info = []; + + if (customer.metadata.containsKey('notes')) { + info.add('Has notes'); + } + if (customer.metadata.containsKey('tags')) { + final tags = customer.metadata['tags']; + if (tags is List && tags.isNotEmpty) { + info.add('${tags.length} tags'); + } + } + if (customer.metadata.containsKey('source')) { + info.add('Source: ${customer.metadata['source']}'); + } + + return info.join(' • '); + } } diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index 877fd96..b92b613 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -199,7 +199,7 @@ class InventoryRoute extends _i18.PageRouteInfo { static _i18.PageInfo page = _i18.PageInfo( name, builder: (data) { - return const _i6.InventoryPage(); + return _i18.WrappedRoute(child: const _i6.InventoryPage()); }, ); }