temp build
This commit is contained in:
21
frontend/pshared/lib/api/responses/payment_method.dart
Normal file
21
frontend/pshared/lib/api/responses/payment_method.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/api/responses/base.dart';
|
||||
import 'package:pshared/api/responses/token.dart';
|
||||
import 'package:pshared/data/dto/payment/method.dart';
|
||||
|
||||
part 'payment_method.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class PaymentMethodResponse extends BaseAuthorizedResponse {
|
||||
|
||||
@JsonKey(name: 'payment_methods')
|
||||
final List<PaymentMethodDTO> paymentMethods;
|
||||
|
||||
const PaymentMethodResponse({required super.accessToken, required this.paymentMethods});
|
||||
|
||||
factory PaymentMethodResponse.fromJson(Map<String, dynamic> json) => _$PaymentMethodResponseFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$PaymentMethodResponseToJson(this);
|
||||
}
|
||||
19
frontend/pshared/lib/api/responses/recipient.dart
Normal file
19
frontend/pshared/lib/api/responses/recipient.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/api/responses/base.dart';
|
||||
import 'package:pshared/api/responses/token.dart';
|
||||
import 'package:pshared/data/dto/recipient/recipient.dart';
|
||||
|
||||
part 'recipient.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class RecipientResponse extends BaseAuthorizedResponse {
|
||||
final List<RecipientDTO> recipients;
|
||||
|
||||
const RecipientResponse({required super.accessToken, required this.recipients});
|
||||
|
||||
factory RecipientResponse.fromJson(Map<String, dynamic> json) => _$RecipientResponseFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$RecipientResponseToJson(this);
|
||||
}
|
||||
@@ -10,6 +10,9 @@ part 'method.g.dart';
|
||||
class PaymentMethodDTO extends PermissionBoundDTO {
|
||||
final String recipientRef;
|
||||
final String type;
|
||||
final String name;
|
||||
final String? description;
|
||||
final bool isMain;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
@@ -24,6 +27,9 @@ class PaymentMethodDTO extends PermissionBoundDTO {
|
||||
required this.recipientRef,
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.name,
|
||||
required this.isMain,
|
||||
this.description,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,20 +10,21 @@ import 'package:pshared/data/mapper/payment/iban.dart';
|
||||
import 'package:pshared/data/mapper/payment/russian_bank.dart';
|
||||
import 'package:pshared/data/mapper/payment/type.dart';
|
||||
import 'package:pshared/data/mapper/payment/wallet.dart';
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/organization/bound.dart';
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
import 'package:pshared/models/payment/payment_method.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
extension PaymentMethodModelMapper on PaymentMethodModel {
|
||||
extension PaymentMethodMapper on PaymentMethod {
|
||||
PaymentMethodDTO toDTO() => PaymentMethodDTO(
|
||||
id: storable.id,
|
||||
createdAt: storable.createdAt,
|
||||
@@ -34,6 +35,9 @@ extension PaymentMethodModelMapper on PaymentMethodModel {
|
||||
type: paymentTypeToValue(type),
|
||||
data: _dataToJson(data),
|
||||
isArchived: isArchived,
|
||||
name: describable.name,
|
||||
description: describable.description,
|
||||
isMain: isMain,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _dataToJson(PaymentMethodData data) {
|
||||
@@ -53,7 +57,7 @@ extension PaymentMethodModelMapper on PaymentMethodModel {
|
||||
}
|
||||
|
||||
extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
||||
PaymentMethodModel toDomain() => PaymentMethodModel(
|
||||
PaymentMethod toDomain() => PaymentMethod(
|
||||
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
||||
permissionBound: newPermissionBound(
|
||||
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
||||
@@ -62,6 +66,8 @@ extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
||||
recipientRef: recipientRef,
|
||||
data: _dataToDomain(paymentTypeFromValue(type), data),
|
||||
isArchived: isArchived,
|
||||
describable: newDescribable(name: name, description: description),
|
||||
isMain: isMain,
|
||||
);
|
||||
|
||||
PaymentMethodData _dataToDomain(PaymentType paymentType, Map<String, dynamic> payload) {
|
||||
|
||||
@@ -2,13 +2,13 @@ import 'package:pshared/data/dto/recipient/recipient.dart';
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/organization/bound.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/recipient/recipient_model.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
extension RecipientModelMapper on RecipientModel {
|
||||
extension RecipientModelMapper on Recipient {
|
||||
RecipientDTO toDTO() => RecipientDTO(
|
||||
id: storable.id,
|
||||
createdAt: storable.createdAt,
|
||||
@@ -26,7 +26,7 @@ extension RecipientModelMapper on RecipientModel {
|
||||
}
|
||||
|
||||
extension RecipientDTOMapper on RecipientDTO {
|
||||
RecipientModel toDomain() => RecipientModel(
|
||||
Recipient toDomain() => Recipient(
|
||||
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
||||
permissionBound: newPermissionBound(
|
||||
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
||||
|
||||
@@ -19,5 +19,15 @@
|
||||
"operationStatusError": "Error",
|
||||
"@operationStatusError": {
|
||||
"description": "Label for the “error” operation status"
|
||||
},
|
||||
|
||||
"resourceLoadError": "Error while loading data. Try again",
|
||||
"@resourceLoadError": {
|
||||
"description": "Default message shown when data loading fails"
|
||||
},
|
||||
|
||||
"resourceEmpty": "Empty data",
|
||||
"@resourceEmpty": {
|
||||
"description": "Default message shown when no data is available"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,15 @@
|
||||
"operationStatusError": "Ошибка",
|
||||
"@operationStatusError": {
|
||||
"description": "Label for the “error” operation status"
|
||||
},
|
||||
|
||||
"resourceLoadError": "Ошибка при загрузке данных. Попробуйте еще раз",
|
||||
"@resourceLoadError": {
|
||||
"description": "Default message shown when data loading fails"
|
||||
},
|
||||
|
||||
"resourceEmpty": "Нет данных",
|
||||
"@resourceEmpty": {
|
||||
"description": "Default message shown when no data is available"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,62 @@
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/permissions/bound/storable.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
class PaymentMethod {
|
||||
PaymentMethod({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.details,
|
||||
required this.type,
|
||||
this.isEnabled = true,
|
||||
|
||||
class PaymentMethod implements PermissionBoundStorable, Describable {
|
||||
final Storable storable;
|
||||
final PermissionBound permissionBound;
|
||||
final Describable describable;
|
||||
final String recipientRef;
|
||||
final PaymentMethodData data;
|
||||
final bool isArchived;
|
||||
final bool isMain;
|
||||
|
||||
const PaymentMethod({
|
||||
required this.storable,
|
||||
required this.permissionBound,
|
||||
required this.describable,
|
||||
required this.recipientRef,
|
||||
required this.data,
|
||||
this.isArchived = false,
|
||||
this.isMain = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String details;
|
||||
final PaymentType type;
|
||||
PaymentType get type => data.type;
|
||||
|
||||
bool isEnabled;
|
||||
bool isMain;
|
||||
}
|
||||
@override
|
||||
String get id => storable.id;
|
||||
@override
|
||||
DateTime get createdAt => storable.createdAt;
|
||||
@override
|
||||
DateTime get updatedAt => storable.updatedAt;
|
||||
|
||||
@override
|
||||
String get organizationRef => permissionBound.organizationRef;
|
||||
@override
|
||||
String get permissionRef => permissionBound.permissionRef;
|
||||
|
||||
@override
|
||||
String get name => describable.name;
|
||||
@override
|
||||
String? get description => describable.description;
|
||||
|
||||
PaymentMethod copyWith({
|
||||
PaymentMethodData? data,
|
||||
bool? isArchived,
|
||||
bool? isMain,
|
||||
Describable? describable,
|
||||
}) => PaymentMethod(
|
||||
storable: storable,
|
||||
permissionBound: permissionBound,
|
||||
recipientRef: recipientRef,
|
||||
data: data ?? this.data,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
isMain: isMain ?? this.isMain,
|
||||
describable: describable ?? this.describable,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
class PaymentMethodModel implements PermissionBound, Storable {
|
||||
final Storable storable;
|
||||
final PermissionBound permissionBound;
|
||||
final String recipientRef;
|
||||
final PaymentMethodData data;
|
||||
final bool isArchived;
|
||||
|
||||
const PaymentMethodModel({
|
||||
required this.storable,
|
||||
required this.permissionBound,
|
||||
required this.recipientRef,
|
||||
required this.data,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
PaymentType get type => data.type;
|
||||
|
||||
@override
|
||||
String get id => storable.id;
|
||||
@override
|
||||
DateTime get createdAt => storable.createdAt;
|
||||
@override
|
||||
DateTime get updatedAt => storable.updatedAt;
|
||||
|
||||
@override
|
||||
String get organizationRef => permissionBound.organizationRef;
|
||||
@override
|
||||
String get permissionRef => permissionBound.permissionRef;
|
||||
|
||||
PaymentMethodModel copyWith({
|
||||
PaymentMethodData? data,
|
||||
bool? isArchived,
|
||||
}) => PaymentMethodModel(
|
||||
storable: storable,
|
||||
permissionBound: permissionBound,
|
||||
recipientRef: recipientRef,
|
||||
data: data ?? this.data,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
);
|
||||
}
|
||||
@@ -1,86 +1,97 @@
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/organization/bound.dart';
|
||||
import 'package:pshared/models/permissions/bound/describable.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
class Recipient {
|
||||
final String? avatarUrl; // network URL / local asset
|
||||
final String name;
|
||||
class Recipient implements PermissionBoundStorableDescribable {
|
||||
final Storable storable;
|
||||
final PermissionBound permissionBound;
|
||||
final Describable describable;
|
||||
final String email;
|
||||
final String? avatarUrl;
|
||||
final RecipientStatus status;
|
||||
final RecipientType type;
|
||||
final CardPaymentMethod? card;
|
||||
final IbanPaymentMethod? iban;
|
||||
final RussianBankAccountPaymentMethod? bank;
|
||||
final WalletPaymentMethod? wallet;
|
||||
final CryptoAddressPaymentMethod? cryptoAddress;
|
||||
final bool isArchived;
|
||||
|
||||
const Recipient({
|
||||
this.avatarUrl,
|
||||
required this.name,
|
||||
required this.storable,
|
||||
required this.permissionBound,
|
||||
required this.describable,
|
||||
required this.email,
|
||||
required this.status,
|
||||
required this.type,
|
||||
this.card,
|
||||
this.iban,
|
||||
this.bank,
|
||||
this.wallet,
|
||||
this.cryptoAddress,
|
||||
this.avatarUrl,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
/// Convenience factory for quickly creating mock recipients.
|
||||
factory Recipient.mock({
|
||||
required String name,
|
||||
required String email,
|
||||
required RecipientStatus status,
|
||||
required RecipientType type,
|
||||
CardPaymentMethod? card,
|
||||
IbanPaymentMethod? iban,
|
||||
RussianBankAccountPaymentMethod? bank,
|
||||
WalletPaymentMethod? wallet,
|
||||
CryptoAddressPaymentMethod? cryptoAddress,
|
||||
}) =>
|
||||
Recipient(
|
||||
avatarUrl: null,
|
||||
name: name,
|
||||
email: email,
|
||||
status: status,
|
||||
type: type,
|
||||
card: card,
|
||||
iban: iban,
|
||||
bank: bank,
|
||||
wallet: wallet,
|
||||
cryptoAddress: cryptoAddress,
|
||||
);
|
||||
@override
|
||||
String get id => storable.id;
|
||||
@override
|
||||
DateTime get createdAt => storable.createdAt;
|
||||
@override
|
||||
DateTime get updatedAt => storable.updatedAt;
|
||||
|
||||
@override
|
||||
String get organizationRef => permissionBound.organizationRef;
|
||||
@override
|
||||
String get permissionRef => permissionBound.permissionRef;
|
||||
|
||||
@override
|
||||
String get name => describable.name;
|
||||
@override
|
||||
String? get description => describable.description;
|
||||
|
||||
Recipient copyWith({
|
||||
Describable? describable,
|
||||
String? email,
|
||||
String? Function()? avatarUrl,
|
||||
RecipientStatus? status,
|
||||
RecipientType? type,
|
||||
bool? isArchived,
|
||||
}) => Recipient(
|
||||
storable: storable,
|
||||
permissionBound: permissionBound,
|
||||
describable: describableCopyWithOther(this.describable, describable),
|
||||
email: email ?? this.email,
|
||||
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
|
||||
status: status ?? this.status,
|
||||
type: type ?? this.type,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
);
|
||||
|
||||
// TODO: move search to backend
|
||||
bool matchesQuery(String q) {
|
||||
final searchable = [
|
||||
name,
|
||||
email,
|
||||
card?.pan,
|
||||
card?.firstName,
|
||||
card?.lastName,
|
||||
iban?.iban,
|
||||
iban?.accountHolder,
|
||||
iban?.bic,
|
||||
iban?.bankName,
|
||||
bank?.accountNumber,
|
||||
bank?.recipientName,
|
||||
bank?.inn,
|
||||
bank?.kpp,
|
||||
bank?.bankName,
|
||||
bank?.bik,
|
||||
bank?.correspondentAccount,
|
||||
wallet?.walletId,
|
||||
cryptoAddress?.address,
|
||||
cryptoAddress?.network,
|
||||
cryptoAddress?.destinationTag,
|
||||
];
|
||||
|
||||
return searchable.any((field) => field?.toLowerCase().contains(q) ?? false);
|
||||
return searchable.any((field) => field.toLowerCase().contains(q.toLowerCase()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Recipient newRecipient({
|
||||
required String organizationRef,
|
||||
required String email,
|
||||
required String name,
|
||||
required RecipientStatus status,
|
||||
required RecipientType type,
|
||||
String? description,
|
||||
String? avatarUrl,
|
||||
bool isArchived = false,
|
||||
}) =>
|
||||
Recipient(
|
||||
storable: newStorable(),
|
||||
permissionBound: newPermissionBound(organizationBound: newOrganizationBound(organizationRef: organizationRef)),
|
||||
describable: newDescribable(name: name, description: description),
|
||||
email: email,
|
||||
status: status,
|
||||
type: type,
|
||||
avatarUrl: avatarUrl,
|
||||
isArchived: isArchived,
|
||||
);
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/permissions/bound/describable.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
class RecipientModel implements PermissionBoundStorableDescribable {
|
||||
final Storable storable;
|
||||
final PermissionBound permissionBound;
|
||||
final Describable describable;
|
||||
final String email;
|
||||
final String? avatarUrl;
|
||||
final RecipientStatus status;
|
||||
final RecipientType type;
|
||||
final bool isArchived;
|
||||
|
||||
const RecipientModel({
|
||||
required this.storable,
|
||||
required this.permissionBound,
|
||||
required this.describable,
|
||||
required this.email,
|
||||
required this.status,
|
||||
required this.type,
|
||||
this.avatarUrl,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String get id => storable.id;
|
||||
@override
|
||||
DateTime get createdAt => storable.createdAt;
|
||||
@override
|
||||
DateTime get updatedAt => storable.updatedAt;
|
||||
|
||||
@override
|
||||
String get organizationRef => permissionBound.organizationRef;
|
||||
@override
|
||||
String get permissionRef => permissionBound.permissionRef;
|
||||
|
||||
@override
|
||||
String get name => describable.name;
|
||||
@override
|
||||
String? get description => describable.description;
|
||||
|
||||
RecipientModel copyWith({
|
||||
Describable? describable,
|
||||
String? email,
|
||||
String? Function()? avatarUrl,
|
||||
RecipientStatus? status,
|
||||
RecipientType? type,
|
||||
bool? isArchived,
|
||||
}) => RecipientModel(
|
||||
storable: storable,
|
||||
permissionBound: permissionBound,
|
||||
describable: describableCopyWithOther(this.describable, describable),
|
||||
email: email ?? this.email,
|
||||
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
|
||||
status: status ?? this.status,
|
||||
type: type ?? this.type,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
);
|
||||
}
|
||||
59
frontend/pshared/lib/provider/recipient/pmethods.dart
Normal file
59
frontend/pshared/lib/provider/recipient/pmethods.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/data/mapper/payment/method.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pshared/provider/template.dart';
|
||||
import 'package:pshared/service/recipient/pmethods.dart';
|
||||
|
||||
|
||||
class PaymentMethodsProvider extends GenericProvider<PaymentMethod> {
|
||||
late OrganizationsProvider _organizations;
|
||||
late RecipientsProvider _recipients;
|
||||
|
||||
PaymentMethodsProvider() : super(service: PaymentMethodService.basicService);
|
||||
|
||||
List<PaymentMethod> get methods => List<PaymentMethod>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
|
||||
|
||||
void updateProviders(OrganizationsProvider organizations, RecipientsProvider recipients) {
|
||||
_organizations = organizations;
|
||||
_recipients = recipients;
|
||||
if (_organizations.isOrganizationSet && (_recipients.currentObject != null)) {
|
||||
load(_organizations.current.id, _recipients.currentObject!.id);
|
||||
}
|
||||
}
|
||||
|
||||
// void reorderMethods(int oldIndex, int newIndex) {
|
||||
// if (newIndex > oldIndex) newIndex--;
|
||||
// final item = _methods.removeAt(oldIndex);
|
||||
// _methods.insert(newIndex, item);
|
||||
// notifyListeners();
|
||||
// }
|
||||
|
||||
PaymentMethod? get main => methods.firstWhereOrNull((m) => m.isMain);
|
||||
|
||||
Future<void> updateMethod(PaymentMethod method) async => update(method.toDTO().toJson());
|
||||
|
||||
Future<void> setArchivedMethod({
|
||||
required PaymentMethod method,
|
||||
required bool newIsArchived,
|
||||
}) async => setArchived(
|
||||
organizationRef: _organizations.current.id,
|
||||
objectRef: method.id,
|
||||
newIsArchived: newIsArchived,
|
||||
cascade: true,
|
||||
);
|
||||
|
||||
|
||||
Future<void> makeMain(PaymentMethod method) {
|
||||
// TODO: create separate backend method to manage main payment method
|
||||
final updates = <Future<void>>[];
|
||||
final currentMain = main;
|
||||
if (currentMain != null) {
|
||||
updates.add(updateMethod(currentMain.copyWith(isMain: false)));
|
||||
}
|
||||
updates.add(updateMethod(method.copyWith(isMain: true)));
|
||||
return Future.wait(updates).then((_) => null);
|
||||
}
|
||||
}
|
||||
60
frontend/pshared/lib/provider/recipient/provider.dart
Normal file
60
frontend/pshared/lib/provider/recipient/provider.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
import 'package:pshared/models/recipient/filter.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/template.dart';
|
||||
import 'package:pshared/service/recipient/service.dart';
|
||||
|
||||
|
||||
class RecipientsProvider extends GenericProvider<Recipient> {
|
||||
late OrganizationsProvider _organizations;
|
||||
|
||||
RecipientFilter _selectedFilter = RecipientFilter.all;
|
||||
String _query = '';
|
||||
|
||||
RecipientFilter get selectedFilter => _selectedFilter;
|
||||
String get query => _query;
|
||||
|
||||
List<Recipient> get recipients => List<Recipient>.unmodifiable(items.toList()..sort((a, b) => a.storable.createdAt.compareTo(b.storable.createdAt)));
|
||||
|
||||
RecipientsProvider() : super(service: RecipientService.basicService);
|
||||
|
||||
List<Recipient> get filteredRecipients {
|
||||
List<Recipient> filtered = recipients.where((r) {
|
||||
switch (_selectedFilter) {
|
||||
case RecipientFilter.ready:
|
||||
return r.status == RecipientStatus.ready;
|
||||
case RecipientFilter.registered:
|
||||
return r.status == RecipientStatus.registered;
|
||||
case RecipientFilter.notRegistered:
|
||||
return r.status == RecipientStatus.notRegistered;
|
||||
case RecipientFilter.all:
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (_query.isNotEmpty) {
|
||||
filtered = filtered.where((r) => r.matchesQuery(_query)).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
void setFilter(RecipientFilter filter) {
|
||||
_selectedFilter = filter;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setQuery(String query) {
|
||||
_query = query.trim().toLowerCase();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateProviders(OrganizationsProvider organizations) {
|
||||
_organizations = organizations;
|
||||
if (_organizations.isOrganizationSet) {
|
||||
load(_organizations.current.id, _organizations.current.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,21 +32,23 @@ List<T> mergeLists<T>({
|
||||
/// to manage state (loading, error, data) without re‑implementing service logic.
|
||||
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
|
||||
final BasicService<T> service;
|
||||
bool _isLoaded = false;
|
||||
|
||||
Resource<List<T>> _resource = Resource(data: []);
|
||||
Resource<List<T>> get resource => _resource;
|
||||
|
||||
List<T> get items => List.unmodifiable(_resource.data ?? []);
|
||||
bool get isLoading => _resource.isLoading;
|
||||
bool get isEmpty => items.isEmpty;
|
||||
Object? get error => _resource.error;
|
||||
bool get isReady => (error == null) && _isLoaded;
|
||||
|
||||
bool get isCurrentSet => _currentObjectRef != null;
|
||||
String? _currentObjectRef; // Stores the currently selected project ref
|
||||
T? get currentObject => _resource.data?.firstWhereOrNull(
|
||||
(object) => object.id == _currentObjectRef,
|
||||
);
|
||||
|
||||
T? getItemById(String id) => items.firstWhereOrNull((item) => item.id == id);
|
||||
T? getItemByRef(String id) => items.firstWhereOrNull((item) => item.id == id);
|
||||
|
||||
GenericProvider({required this.service});
|
||||
|
||||
@@ -67,11 +69,13 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadFuture(Future<List<T>> future) async {
|
||||
Future<List<T>> loadFuture(Future<List<T>> future) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final list = await future;
|
||||
_isLoaded = true;
|
||||
_setResource(Resource(data: list, isLoading: false));
|
||||
return list;
|
||||
} catch (e) {
|
||||
_setResource(
|
||||
_resource.copyWith(isLoading: false, error: toException(e)),
|
||||
@@ -80,17 +84,30 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> load(String organizationRef, String? parentRef) async {
|
||||
Future<void> load(
|
||||
String organizationRef,
|
||||
String? parentRef, {
|
||||
int? limit,
|
||||
int? offset,
|
||||
bool? Function()? fetchArchived,
|
||||
}) async {
|
||||
if (parentRef != null) {
|
||||
return loadFuture(service.list(organizationRef, parentRef));
|
||||
await loadFuture(
|
||||
service.list(
|
||||
organizationRef,
|
||||
parentRef,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
fetchArchived: fetchArchived == null ? null : fetchArchived(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadItem(String itemRef) async {
|
||||
return loadFuture((() async => [await service.get(itemRef)])());
|
||||
await loadFuture((() async => [await service.get(itemRef)])());
|
||||
}
|
||||
|
||||
|
||||
List<T> merge(List<T> rhs) => mergeLists<T>(
|
||||
lhs: items,
|
||||
rhs: rhs,
|
||||
@@ -134,11 +151,47 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(String objectRef) async {
|
||||
Future<void> delete(String objectRef, {Map<String, dynamic>? request}) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
|
||||
try {
|
||||
await service.delete(objectRef);
|
||||
await service.delete(objectRef, request: request);
|
||||
if (_currentObjectRef == objectRef) {
|
||||
_currentObjectRef = null;
|
||||
}
|
||||
|
||||
_setResource(Resource(
|
||||
data: _resource.data?.where((p) => p.id != objectRef).toList(),
|
||||
isLoading: false,
|
||||
));
|
||||
} catch (e) {
|
||||
_setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleArchived(T item, bool currentState, {bool? cascade}) => setArchived(
|
||||
organizationRef: item.organizationRef,
|
||||
objectRef: item.id,
|
||||
newIsArchived: !currentState,
|
||||
cascade: cascade ?? true,
|
||||
);
|
||||
|
||||
Future<void> setArchived({
|
||||
required String organizationRef,
|
||||
required String objectRef,
|
||||
required bool newIsArchived,
|
||||
bool? cascade,
|
||||
}) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
|
||||
try {
|
||||
await service.archive(
|
||||
organizationRef: organizationRef,
|
||||
objectRef: objectRef,
|
||||
newIsArchived: newIsArchived,
|
||||
cascade: cascade,
|
||||
);
|
||||
if (_currentObjectRef == objectRef) {
|
||||
_currentObjectRef = null;
|
||||
}
|
||||
@@ -154,11 +207,17 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
||||
}
|
||||
|
||||
bool setCurrentObject(String? objectRef) {
|
||||
if (_currentObjectRef == objectRef) {
|
||||
// No change, skip notification
|
||||
return true;
|
||||
}
|
||||
|
||||
if (objectRef == null) {
|
||||
_currentObjectRef = null;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
|
||||
_currentObjectRef = objectRef;
|
||||
notifyListeners();
|
||||
@@ -167,4 +226,5 @@ class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier
|
||||
|
||||
return false; // Object not found
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
37
frontend/pshared/lib/service/recipient/pmethods.dart
Normal file
37
frontend/pshared/lib/service/recipient/pmethods.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:pshared/api/responses/payment_method.dart';
|
||||
import 'package:pshared/data/mapper/payment/method.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/service/services.dart';
|
||||
import 'package:pshared/service/template.dart';
|
||||
|
||||
|
||||
class PaymentMethodService {
|
||||
static const String _objectType = Services.paymentMethods;
|
||||
|
||||
static final BasicService<PaymentMethod> _basicService = BasicService<PaymentMethod>(
|
||||
objectType: _objectType,
|
||||
fromJson: (json) => PaymentMethodResponse.fromJson(json).paymentMethods.map((dto) => dto.toDomain()).toList(),
|
||||
);
|
||||
|
||||
static BasicService<PaymentMethod> get basicService => _basicService;
|
||||
|
||||
static Future<List<PaymentMethod>> list(String organizationRef, String recipientRef) async {
|
||||
return _basicService.list(organizationRef, recipientRef);
|
||||
}
|
||||
|
||||
static Future<PaymentMethod> get(String recipientRef) async {
|
||||
return _basicService.get(recipientRef);
|
||||
}
|
||||
|
||||
static Future<List<PaymentMethod>> create(String organizationRef, PaymentMethod paymentMethod) async {
|
||||
return _basicService.create(organizationRef, paymentMethod.toDTO().toJson());
|
||||
}
|
||||
|
||||
static Future<List<PaymentMethod>> update(PaymentMethod paymentMethod) async {
|
||||
return _basicService.update(paymentMethod.toDTO().toJson());
|
||||
}
|
||||
|
||||
static Future<List<PaymentMethod>> delete(PaymentMethod paymentMethod) async {
|
||||
return _basicService.delete(paymentMethod.storable.id);
|
||||
}
|
||||
}
|
||||
37
frontend/pshared/lib/service/recipient/service.dart
Normal file
37
frontend/pshared/lib/service/recipient/service.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:pshared/api/responses/recipient.dart';
|
||||
import 'package:pshared/data/mapper/recipient/recipient.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/service/services.dart';
|
||||
import 'package:pshared/service/template.dart';
|
||||
|
||||
|
||||
class RecipientService {
|
||||
static const String _objectType = Services.recipients;
|
||||
|
||||
static final BasicService<Recipient> _basicService = BasicService<Recipient>(
|
||||
objectType: _objectType,
|
||||
fromJson: (json) => RecipientResponse.fromJson(json).recipients.map((dto) => dto.toDomain()).toList(),
|
||||
);
|
||||
|
||||
static BasicService<Recipient> get basicService => _basicService;
|
||||
|
||||
static Future<List<Recipient>> list(String organizationRef, String _) async {
|
||||
return _basicService.list(organizationRef, organizationRef);
|
||||
}
|
||||
|
||||
static Future<Recipient> get(String recipientRef) async {
|
||||
return _basicService.get(recipientRef);
|
||||
}
|
||||
|
||||
static Future<List<Recipient>> create(String organizationRef, Recipient recipient) async {
|
||||
return _basicService.create(organizationRef, recipient.toDTO().toJson());
|
||||
}
|
||||
|
||||
static Future<List<Recipient>> update(Recipient recipient) async {
|
||||
return _basicService.update(recipient.toDTO().toJson());
|
||||
}
|
||||
|
||||
static Future<List<Recipient>> delete(Recipient recipient) async {
|
||||
return _basicService.delete(recipient.storable.id);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:pshared/service/authorization/service.dart';
|
||||
import 'package:pshared/utils/http/params.dart';
|
||||
|
||||
|
||||
class BasicService<T> {
|
||||
@@ -15,15 +16,26 @@ class BasicService<T> {
|
||||
required this.fromJson,
|
||||
}) : _objectType = objectType, _logger = Logger('service.$objectType');
|
||||
|
||||
Future<List<T>> list(String organizationRef, String parentRef) async {
|
||||
_logger.fine('Loading all objects');
|
||||
String _refLog(String ref) => ref.isEmpty ? '<not set>' : ref;
|
||||
|
||||
Future<List<T>> list(String organizationRef, String parentRef, {int? limit, int? offset, bool? fetchArchived}) async {
|
||||
_logger.fine('Loading all for organization ${_refLog(organizationRef)} and parent ${_refLog(organizationRef)} with: limit=${_formatParameter(limit)}, offset=${_formatParameter(offset)}, fetchArchived=${_formatParameter(fetchArchived)}...');
|
||||
|
||||
return _getObjects(
|
||||
AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef/$parentRef'),
|
||||
AuthorizationService.getGETResponse(
|
||||
_objectType,
|
||||
paramsToUriString(
|
||||
path: '/list/$organizationRef/$parentRef',
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
fetchArchived: fetchArchived,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> get(String objectRef) async {
|
||||
_logger.fine('Loading object $objectRef');
|
||||
_logger.fine('Loading $_objectType $objectRef');
|
||||
final objects = await _getObjects(
|
||||
AuthorizationService.getGETResponse(_objectType, '/$objectRef'),
|
||||
);
|
||||
@@ -31,24 +43,36 @@ class BasicService<T> {
|
||||
}
|
||||
|
||||
Future<List<T>> create(String organizationRef, Map<String, dynamic> request) async {
|
||||
_logger.fine('Creating new object...');
|
||||
_logger.fine('Creating new...');
|
||||
return _getObjects(
|
||||
AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<T>> update(Map<String, dynamic> request) async {
|
||||
_logger.fine('Patching object...');
|
||||
_logger.fine('Patching...');
|
||||
return _getObjects(
|
||||
AuthorizationService.getPUTResponse(_objectType, '/', request,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<T>> delete(String objecRef) async {
|
||||
_logger.fine('Deleting object $objecRef');
|
||||
Future<List<T>> delete(String objecRef, {Map<String, dynamic>? request}) async {
|
||||
_logger.fine('Deleting $_objectType $objecRef');
|
||||
return _getObjects(
|
||||
AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', {}),
|
||||
AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', request ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<T>> archive({
|
||||
required String organizationRef,
|
||||
required String objectRef,
|
||||
required bool newIsArchived,
|
||||
bool? cascade,
|
||||
}) async {
|
||||
_logger.fine('Setting new archive status $newIsArchived to $objectRef');
|
||||
return _getObjects(
|
||||
AuthorizationService.getGETResponse(_objectType, '/archive/$organizationRef/$objectRef?archived=$newIsArchived&cascade=${cascade ?? false}'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,8 +83,12 @@ class BasicService<T> {
|
||||
_logger.fine('Fetched ${objects.length} object(s)');
|
||||
return objects;
|
||||
} catch (e, stackTrace) {
|
||||
_logger.severe('Failed to fetch objects', e, stackTrace);
|
||||
_logger.severe('Failed to fetch', e, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatParameter(dynamic value) {
|
||||
return value?.toString() ?? '<not specified>';
|
||||
}
|
||||
}
|
||||
|
||||
38
frontend/pshared/lib/utils/http/params.dart
Normal file
38
frontend/pshared/lib/utils/http/params.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
// Query parameter constants
|
||||
const String _limitParam = 'limit';
|
||||
const String _offsetParam = 'offset';
|
||||
const String _archivedParam = 'archived';
|
||||
|
||||
void _addIfNotNull(Map<String, String> params, String key, dynamic value) {
|
||||
if (value != null) {
|
||||
params[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Uri paramsToUri({
|
||||
required String path,
|
||||
int? limit,
|
||||
int? offset,
|
||||
bool? fetchArchived,
|
||||
Map<String, String>? params,
|
||||
}) {
|
||||
Map<String, String> queryParams = params ?? {};
|
||||
_addIfNotNull(queryParams, _limitParam, limit);
|
||||
_addIfNotNull(queryParams, _offsetParam, offset);
|
||||
_addIfNotNull(queryParams, _archivedParam, fetchArchived);
|
||||
|
||||
// Build URL with query parameters using Uri class
|
||||
return Uri(
|
||||
path: path,
|
||||
queryParameters: queryParams.isEmpty ? null : queryParams,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
String paramsToUriString({
|
||||
required String path,
|
||||
Map<String, String> queryParams = const {},
|
||||
int? limit,
|
||||
int? offset,
|
||||
bool? fetchArchived,
|
||||
}) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString();
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/generated/i18n/ps_localizations.dart';
|
||||
import 'package:pshared/provider/template.dart';
|
||||
|
||||
|
||||
@@ -20,9 +21,10 @@ class ResourceContainer<T extends GenericProvider> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Consumer<T>(builder: (context, provider, _) {
|
||||
if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator());
|
||||
if (provider.error != null) return error ?? Text('Error while loading data. Try again');
|
||||
if (provider.isEmpty) return empty ?? Text('Empty data');
|
||||
final l10n = PSLocalizations.of(context)!;
|
||||
if (provider.isLoading) return loading ?? const Center(child: CircularProgressIndicator());
|
||||
if (provider.error != null) return error ?? Text(l10n.resourceLoadError);
|
||||
if (provider.items.isEmpty) return empty ?? Text(l10n.resourceEmpty);
|
||||
return builder(context, provider);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user