temp build

This commit is contained in:
Stephan D
2025-12-05 01:32:41 +01:00
parent 082d782a80
commit f71cc76f64
50 changed files with 853 additions and 707 deletions

View 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);
}

View 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);
}

View File

@@ -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,
});

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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,
);
}

View File

@@ -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,
);
}

View File

@@ -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,
);

View File

@@ -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,
);
}

View 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);
}
}

View 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);
}
}
}

View File

@@ -32,21 +32,23 @@ List<T> mergeLists<T>({
/// to manage state (loading, error, data) without reimplementing 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
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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>';
}
}

View 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();

View File

@@ -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);
});
}