feat: customer page
This commit is contained in:
parent
d22ffdd6d0
commit
51289d7829
@ -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<CustomerLoaderEvent, CustomerLoaderState> {
|
||||
final ICustomerRepository _repository;
|
||||
CustomerLoaderBloc(this._repository) : super(CustomerLoaderState.initial()) {
|
||||
on<CustomerLoaderEvent>(_onCustomerLoaderEvent);
|
||||
}
|
||||
|
||||
Future<void> _onCustomerLoaderEvent(
|
||||
CustomerLoaderEvent event,
|
||||
Emitter<CustomerLoaderState> 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<CustomerLoaderState> _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;
|
||||
}
|
||||
}
|
||||
@ -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>(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<TResult extends Object?>({
|
||||
required TResult Function(String search) searchChanged,
|
||||
required TResult Function(bool isRefresh) fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(String search)? searchChanged,
|
||||
TResult? Function(bool isRefresh)? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(String search)? searchChanged,
|
||||
TResult Function(bool isRefresh)? fetched,
|
||||
required TResult orElse(),
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_SearchChanged value) searchChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_SearchChanged value)? searchChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
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<TResult extends Object?>({
|
||||
required TResult Function(String search) searchChanged,
|
||||
required TResult Function(bool isRefresh) fetched,
|
||||
}) {
|
||||
return searchChanged(search);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(String search)? searchChanged,
|
||||
TResult? Function(bool isRefresh)? fetched,
|
||||
}) {
|
||||
return searchChanged?.call(search);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
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<TResult extends Object?>({
|
||||
required TResult Function(_SearchChanged value) searchChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) {
|
||||
return searchChanged(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_SearchChanged value)? searchChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) {
|
||||
return searchChanged?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
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<TResult extends Object?>({
|
||||
required TResult Function(String search) searchChanged,
|
||||
required TResult Function(bool isRefresh) fetched,
|
||||
}) {
|
||||
return fetched(isRefresh);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(String search)? searchChanged,
|
||||
TResult? Function(bool isRefresh)? fetched,
|
||||
}) {
|
||||
return fetched?.call(isRefresh);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
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<TResult extends Object?>({
|
||||
required TResult Function(_SearchChanged value) searchChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) {
|
||||
return fetched(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_SearchChanged value)? searchChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) {
|
||||
return fetched?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
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<Customer> get customers => throw _privateConstructorUsedError;
|
||||
Option<CustomerFailure> 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<CustomerLoaderState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $CustomerLoaderStateCopyWith<$Res> {
|
||||
factory $CustomerLoaderStateCopyWith(
|
||||
CustomerLoaderState value,
|
||||
$Res Function(CustomerLoaderState) then,
|
||||
) = _$CustomerLoaderStateCopyWithImpl<$Res, CustomerLoaderState>;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<Customer> customers,
|
||||
Option<CustomerFailure> 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<Customer>,
|
||||
failureOptionCustomer: null == failureOptionCustomer
|
||||
? _value.failureOptionCustomer
|
||||
: failureOptionCustomer // ignore: cast_nullable_to_non_nullable
|
||||
as Option<CustomerFailure>,
|
||||
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<Customer> customers,
|
||||
Option<CustomerFailure> 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<Customer>,
|
||||
failureOptionCustomer: null == failureOptionCustomer
|
||||
? _value.failureOptionCustomer
|
||||
: failureOptionCustomer // ignore: cast_nullable_to_non_nullable
|
||||
as Option<CustomerFailure>,
|
||||
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<Customer> customers,
|
||||
required this.failureOptionCustomer,
|
||||
this.categoryId,
|
||||
this.search,
|
||||
this.isFetching = false,
|
||||
this.hasReachedMax = false,
|
||||
this.page = 1,
|
||||
}) : _customers = customers;
|
||||
|
||||
final List<Customer> _customers;
|
||||
@override
|
||||
List<Customer> get customers {
|
||||
if (_customers is EqualUnmodifiableListView) return _customers;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_customers);
|
||||
}
|
||||
|
||||
@override
|
||||
final Option<CustomerFailure> 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<Customer> customers,
|
||||
required final Option<CustomerFailure> failureOptionCustomer,
|
||||
final String? categoryId,
|
||||
final String? search,
|
||||
final bool isFetching,
|
||||
final bool hasReachedMax,
|
||||
final int page,
|
||||
}) = _$CustomerLoaderStateImpl;
|
||||
|
||||
@override
|
||||
List<Customer> get customers;
|
||||
@override
|
||||
Option<CustomerFailure> 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
part of 'customer_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class CustomerLoaderState with _$CustomerLoaderState {
|
||||
const factory CustomerLoaderState({
|
||||
required List<Customer> customers,
|
||||
required Option<CustomerFailure> 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());
|
||||
}
|
||||
@ -17,4 +17,7 @@ class ApiPath {
|
||||
|
||||
// Product
|
||||
static const String product = '/api/v1/products';
|
||||
|
||||
// Customer
|
||||
static const String customer = '/api/v1/customers';
|
||||
}
|
||||
|
||||
9
lib/domain/customer/customer.dart
Normal file
9
lib/domain/customer/customer.dart
Normal file
@ -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';
|
||||
1015
lib/domain/customer/customer.freezed.dart
Normal file
1015
lib/domain/customer/customer.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
32
lib/domain/customer/entities/customer_entity.dart
Normal file
32
lib/domain/customer/entities/customer_entity.dart
Normal file
@ -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<String, dynamic> 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: '',
|
||||
);
|
||||
}
|
||||
10
lib/domain/customer/failures/customer_failure.dart
Normal file
10
lib/domain/customer/failures/customer_failure.dart
Normal file
@ -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;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
part of '../customer.dart';
|
||||
|
||||
abstract class ICustomerRepository {
|
||||
Future<Either<CustomerFailure, List<Customer>>> get({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
String? search,
|
||||
});
|
||||
}
|
||||
8
lib/infrastructure/customer/customer_dtos.dart
Normal file
8
lib/infrastructure/customer/customer_dtos.dart
Normal file
@ -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';
|
||||
440
lib/infrastructure/customer/customer_dtos.freezed.dart
Normal file
440
lib/infrastructure/customer/customer_dtos.freezed.dart
Normal file
@ -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>(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<String, dynamic> 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<String, dynamic>? 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<String, dynamic> 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<CustomerDto> 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<String, dynamic>? 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<String, dynamic>?,
|
||||
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<String, dynamic>? 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<String, dynamic>?,
|
||||
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<String, dynamic>? metadata,
|
||||
@JsonKey(name: "created_at") this.createdAt,
|
||||
@JsonKey(name: "updated_at") this.updatedAt,
|
||||
}) : _metadata = metadata,
|
||||
super._();
|
||||
|
||||
factory _$CustomerDtoImpl.fromJson(Map<String, dynamic> 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<String, dynamic>? _metadata;
|
||||
@override
|
||||
@JsonKey(name: "metadata")
|
||||
Map<String, dynamic>? 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<String, dynamic> 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<String, dynamic>? metadata,
|
||||
@JsonKey(name: "created_at") final String? createdAt,
|
||||
@JsonKey(name: "updated_at") final String? updatedAt,
|
||||
}) = _$CustomerDtoImpl;
|
||||
const _CustomerDto._() : super._();
|
||||
|
||||
factory _CustomerDto.fromJson(Map<String, dynamic> 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<String, dynamic>? 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;
|
||||
}
|
||||
37
lib/infrastructure/customer/customer_dtos.g.dart
Normal file
37
lib/infrastructure/customer/customer_dtos.g.dart
Normal file
@ -0,0 +1,37 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'customer_dtos.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$CustomerDtoImpl _$$CustomerDtoImplFromJson(Map<String, dynamic> 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<String, dynamic>?,
|
||||
createdAt: json['created_at'] as String?,
|
||||
updatedAt: json['updated_at'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$CustomerDtoImplToJson(_$CustomerDtoImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'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,
|
||||
};
|
||||
@ -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<DC<CustomerFailure, List<CustomerDto>>> fetch({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
String? search,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
39
lib/infrastructure/customer/dto/customer_dto.dart
Normal file
39
lib/infrastructure/customer/dto/customer_dto.dart
Normal file
@ -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<String, dynamic>? metadata,
|
||||
@JsonKey(name: "created_at") String? createdAt,
|
||||
@JsonKey(name: "updated_at") String? updatedAt,
|
||||
}) = _CustomerDto;
|
||||
|
||||
factory CustomerDto.fromJson(Map<String, dynamic> 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 ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<Either<CustomerFailure, List<Customer>>> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>(),
|
||||
|
||||
@ -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<CustomerPage> createState() => _CustomerPageState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) => BlocProvider(
|
||||
create: (context) =>
|
||||
getIt<CustomerLoaderBloc>()
|
||||
..add(CustomerLoaderEvent.fetched(isRefresh: true)),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
class _CustomerPageState extends State<CustomerPage>
|
||||
with TickerProviderStateMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
ScrollController _scrollController = ScrollController();
|
||||
bool _isGridView = false;
|
||||
|
||||
// Sample customer data
|
||||
final List<Customer> _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<CustomerPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Customer> 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<CustomerLoaderBloc, CustomerLoaderState>(
|
||||
builder: (context, state) {
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification is ScrollEndNotification &&
|
||||
_scrollController.position.extentAfter == 0) {
|
||||
context.read<CustomerLoaderBloc>().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<Customer> 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<Customer> customers) {
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.all(16),
|
||||
sliver: SliverGrid(
|
||||
@ -210,9 +151,9 @@ class _CustomerPageState extends State<CustomerPage>
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = <String>[];
|
||||
|
||||
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(' • ');
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ class InventoryRoute extends _i18.PageRouteInfo<void> {
|
||||
static _i18.PageInfo page = _i18.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i6.InventoryPage();
|
||||
return _i18.WrappedRoute(child: const _i6.InventoryPage());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user