Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7498ab880d |
@@ -1,22 +0,0 @@
|
|||||||
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/invitation/invitation.dart';
|
|
||||||
|
|
||||||
part 'invitations.g.dart';
|
|
||||||
|
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
|
||||||
class InvitationsResponse extends BaseAuthorizedResponse {
|
|
||||||
final List<InvitationDTO> invitations;
|
|
||||||
|
|
||||||
const InvitationsResponse({
|
|
||||||
required super.accessToken,
|
|
||||||
required this.invitations,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory InvitationsResponse.fromJson(Map<String, dynamic> json) => _$InvitationsResponseFromJson(json);
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$InvitationsResponseToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/data/dto/date_time.dart';
|
|
||||||
import 'package:pshared/data/dto/permissions/bound.dart';
|
|
||||||
|
|
||||||
part 'invitation.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class InvitationContentDTO {
|
|
||||||
final String email;
|
|
||||||
final String name;
|
|
||||||
final String comment;
|
|
||||||
|
|
||||||
const InvitationContentDTO({
|
|
||||||
required this.email,
|
|
||||||
required this.name,
|
|
||||||
required this.comment,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory InvitationContentDTO.fromJson(Map<String, dynamic> json) => _$InvitationContentDTOFromJson(json);
|
|
||||||
Map<String, dynamic> toJson() => _$InvitationContentDTOToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
|
||||||
class InvitationDTO extends PermissionBoundDTO {
|
|
||||||
final String roleRef;
|
|
||||||
final String inviterRef;
|
|
||||||
final String status;
|
|
||||||
|
|
||||||
@UtcIso8601Converter()
|
|
||||||
final DateTime expiresAt;
|
|
||||||
|
|
||||||
@JsonKey(name: 'description')
|
|
||||||
final InvitationContentDTO content;
|
|
||||||
|
|
||||||
@JsonKey(defaultValue: false)
|
|
||||||
final bool isArchived;
|
|
||||||
|
|
||||||
const InvitationDTO({
|
|
||||||
required super.id,
|
|
||||||
required super.createdAt,
|
|
||||||
required super.updatedAt,
|
|
||||||
required super.permissionRef,
|
|
||||||
required super.organizationRef,
|
|
||||||
required this.roleRef,
|
|
||||||
required this.inviterRef,
|
|
||||||
required this.status,
|
|
||||||
required this.expiresAt,
|
|
||||||
required this.content,
|
|
||||||
this.isArchived = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory InvitationDTO.fromJson(Map<String, dynamic> json) => _$InvitationDTOFromJson(json);
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() => _$InvitationDTOToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import 'package:pshared/data/dto/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/status.dart';
|
|
||||||
import 'package:pshared/models/organization/bound.dart';
|
|
||||||
import 'package:pshared/models/permissions/bound.dart';
|
|
||||||
import 'package:pshared/models/storable.dart';
|
|
||||||
|
|
||||||
|
|
||||||
extension InvitationModelMapper on Invitation {
|
|
||||||
InvitationDTO toDTO() => InvitationDTO(
|
|
||||||
id: storable.id,
|
|
||||||
createdAt: storable.createdAt,
|
|
||||||
updatedAt: storable.updatedAt,
|
|
||||||
permissionRef: permissionBound.permissionRef,
|
|
||||||
organizationRef: permissionBound.organizationRef,
|
|
||||||
roleRef: roleRef,
|
|
||||||
inviterRef: inviterRef,
|
|
||||||
status: _statusToValue(status),
|
|
||||||
expiresAt: expiresAt,
|
|
||||||
content: InvitationContentDTO(
|
|
||||||
email: content.email,
|
|
||||||
name: content.name,
|
|
||||||
comment: content.comment,
|
|
||||||
),
|
|
||||||
isArchived: isArchived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InvitationDTOMapper on InvitationDTO {
|
|
||||||
Invitation toDomain() => Invitation(
|
|
||||||
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
|
|
||||||
permissionBound: newPermissionBound(
|
|
||||||
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
|
||||||
permissionRef: permissionRef,
|
|
||||||
),
|
|
||||||
roleRef: roleRef,
|
|
||||||
inviterRef: inviterRef,
|
|
||||||
status: _statusFromValue(status),
|
|
||||||
expiresAt: expiresAt.toUtc(),
|
|
||||||
content: InvitationContent(
|
|
||||||
email: content.email,
|
|
||||||
name: content.name,
|
|
||||||
comment: content.comment,
|
|
||||||
),
|
|
||||||
isArchived: isArchived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InvitationStatus _statusFromValue(String value) {
|
|
||||||
switch (value) {
|
|
||||||
case 'sent':
|
|
||||||
return InvitationStatus.sent;
|
|
||||||
case 'accepted':
|
|
||||||
return InvitationStatus.accepted;
|
|
||||||
case 'declined':
|
|
||||||
return InvitationStatus.declined;
|
|
||||||
case 'revoked':
|
|
||||||
return InvitationStatus.revoked;
|
|
||||||
case 'created':
|
|
||||||
default:
|
|
||||||
return InvitationStatus.created;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _statusToValue(InvitationStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case InvitationStatus.sent:
|
|
||||||
return 'sent';
|
|
||||||
case InvitationStatus.accepted:
|
|
||||||
return 'accepted';
|
|
||||||
case InvitationStatus.declined:
|
|
||||||
return 'declined';
|
|
||||||
case InvitationStatus.revoked:
|
|
||||||
return 'revoked';
|
|
||||||
case InvitationStatus.created:
|
|
||||||
return 'created';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import 'package:pshared/models/organization/bound.dart';
|
|
||||||
import 'package:pshared/models/permissions/bound.dart';
|
|
||||||
import 'package:pshared/models/permissions/bound/storable.dart';
|
|
||||||
import 'package:pshared/models/storable.dart';
|
|
||||||
import 'package:pshared/models/invitation/status.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationContent {
|
|
||||||
final String email;
|
|
||||||
final String name;
|
|
||||||
final String comment;
|
|
||||||
|
|
||||||
const InvitationContent({
|
|
||||||
required this.email,
|
|
||||||
required this.name,
|
|
||||||
required this.comment,
|
|
||||||
});
|
|
||||||
|
|
||||||
InvitationContent copyWith({
|
|
||||||
String? email,
|
|
||||||
String? name,
|
|
||||||
String? comment,
|
|
||||||
}) => InvitationContent(
|
|
||||||
email: email ?? this.email,
|
|
||||||
name: name ?? this.name,
|
|
||||||
comment: comment ?? this.comment,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Invitation implements PermissionBoundStorable {
|
|
||||||
final Storable storable;
|
|
||||||
final PermissionBound permissionBound;
|
|
||||||
final String roleRef;
|
|
||||||
final String inviterRef;
|
|
||||||
final InvitationStatus status;
|
|
||||||
final DateTime expiresAt;
|
|
||||||
final InvitationContent content;
|
|
||||||
final bool isArchived;
|
|
||||||
|
|
||||||
Invitation({
|
|
||||||
required this.storable,
|
|
||||||
required this.permissionBound,
|
|
||||||
required this.roleRef,
|
|
||||||
required this.inviterRef,
|
|
||||||
required this.status,
|
|
||||||
required this.expiresAt,
|
|
||||||
required this.content,
|
|
||||||
this.isArchived = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get id => storable.id;
|
|
||||||
@override
|
|
||||||
DateTime get createdAt => storable.createdAt;
|
|
||||||
@override
|
|
||||||
DateTime get updatedAt => storable.updatedAt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get permissionRef => permissionBound.permissionRef;
|
|
||||||
@override
|
|
||||||
String get organizationRef => permissionBound.organizationRef;
|
|
||||||
|
|
||||||
String get inviteeDisplayName => content.name.isNotEmpty ? content.name : content.email;
|
|
||||||
bool get isExpired => expiresAt.isBefore(DateTime.now().toUtc());
|
|
||||||
bool get isPending => status == InvitationStatus.created || status == InvitationStatus.sent;
|
|
||||||
|
|
||||||
Invitation copyWith({
|
|
||||||
Storable? storable,
|
|
||||||
PermissionBound? permissionBound,
|
|
||||||
String? roleRef,
|
|
||||||
String? inviterRef,
|
|
||||||
InvitationStatus? status,
|
|
||||||
DateTime? expiresAt,
|
|
||||||
InvitationContent? content,
|
|
||||||
bool? isArchived,
|
|
||||||
}) => Invitation(
|
|
||||||
storable: storable ?? this.storable,
|
|
||||||
permissionBound: permissionBound ?? this.permissionBound,
|
|
||||||
roleRef: roleRef ?? this.roleRef,
|
|
||||||
inviterRef: inviterRef ?? this.inviterRef,
|
|
||||||
status: status ?? this.status,
|
|
||||||
expiresAt: expiresAt ?? this.expiresAt,
|
|
||||||
content: content ?? this.content,
|
|
||||||
isArchived: isArchived ?? this.isArchived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Invitation newInvitation({
|
|
||||||
required String organizationRef,
|
|
||||||
required String roleRef,
|
|
||||||
required String inviterRef,
|
|
||||||
required String email,
|
|
||||||
String name = '',
|
|
||||||
String comment = '',
|
|
||||||
InvitationStatus status = InvitationStatus.created,
|
|
||||||
DateTime? expiresAt,
|
|
||||||
bool isArchived = false,
|
|
||||||
String? permissionRef,
|
|
||||||
}) => Invitation(
|
|
||||||
storable: newStorable(),
|
|
||||||
permissionBound: newPermissionBound(
|
|
||||||
organizationBound: newOrganizationBound(organizationRef: organizationRef),
|
|
||||||
permissionRef: permissionRef,
|
|
||||||
),
|
|
||||||
roleRef: roleRef,
|
|
||||||
inviterRef: inviterRef,
|
|
||||||
status: status,
|
|
||||||
expiresAt: expiresAt ?? DateTime.now().toUtc().add(const Duration(days: 7)),
|
|
||||||
content: InvitationContent(email: email, name: name, comment: comment),
|
|
||||||
isArchived: isArchived,
|
|
||||||
);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
enum InvitationStatus {
|
|
||||||
created,
|
|
||||||
sent,
|
|
||||||
accepted,
|
|
||||||
declined,
|
|
||||||
revoked,
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,8 @@ class CardPaymentMethod implements PaymentMethodData {
|
|||||||
|
|
||||||
const CardPaymentMethod({
|
const CardPaymentMethod({
|
||||||
required this.pan,
|
required this.pan,
|
||||||
required this.expMonth,
|
this.expMonth,
|
||||||
required this.expYear,
|
this.expYear,
|
||||||
required this.firstName,
|
required this.firstName,
|
||||||
required this.lastName,
|
required this.lastName,
|
||||||
this.country,
|
this.country,
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'package:pshared/data/mapper/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/status.dart';
|
|
||||||
import 'package:pshared/provider/organizations.dart';
|
|
||||||
import 'package:pshared/provider/template.dart';
|
|
||||||
import 'package:pshared/service/invitation/service.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationsProvider extends GenericProvider<Invitation> {
|
|
||||||
InvitationsProvider() : super(service: InvitationService.basicService);
|
|
||||||
|
|
||||||
late OrganizationsProvider _organizations;
|
|
||||||
String? _loadedOrganizationId;
|
|
||||||
|
|
||||||
List<Invitation> get invitations => List<Invitation>.unmodifiable(items);
|
|
||||||
|
|
||||||
void updateProviders(OrganizationsProvider organizations) {
|
|
||||||
_organizations = organizations;
|
|
||||||
if (_organizations.isOrganizationSet) {
|
|
||||||
final organizationId = _organizations.current.id;
|
|
||||||
if (_loadedOrganizationId != organizationId) {
|
|
||||||
_loadedOrganizationId = organizationId;
|
|
||||||
load(organizationId, organizationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Invitation> sendInvitation({
|
|
||||||
required String email,
|
|
||||||
required String roleRef,
|
|
||||||
required String inviterRef,
|
|
||||||
String name = '',
|
|
||||||
String comment = '',
|
|
||||||
DateTime? expiresAt,
|
|
||||||
}) async {
|
|
||||||
final invitation = newInvitation(
|
|
||||||
organizationRef: _organizations.current.id,
|
|
||||||
roleRef: roleRef,
|
|
||||||
inviterRef: inviterRef,
|
|
||||||
email: email,
|
|
||||||
name: name,
|
|
||||||
comment: comment,
|
|
||||||
expiresAt: expiresAt,
|
|
||||||
);
|
|
||||||
|
|
||||||
return createObject(_organizations.current.id, invitation.toDTO().toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateInvitation(Invitation invitation) {
|
|
||||||
return update(invitation.toDTO().toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> revokeInvitation(Invitation invitation) {
|
|
||||||
return updateInvitation(invitation.copyWith(status: InvitationStatus.revoked));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setInvitationArchived(Invitation invitation, bool archived) {
|
|
||||||
return setArchived(
|
|
||||||
organizationRef: _organizations.current.id,
|
|
||||||
objectRef: invitation.id,
|
|
||||||
newIsArchived: archived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -165,7 +165,6 @@ class PermissionsProvider extends ChangeNotifier {
|
|||||||
perm.Action? action,
|
perm.Action? action,
|
||||||
Object? objectRef,
|
Object? objectRef,
|
||||||
}) {
|
}) {
|
||||||
if (!_organizations.isOrganizationSet) return false;
|
|
||||||
final orgId = _organizations.current.id;
|
final orgId = _organizations.current.id;
|
||||||
final pd = policyDescriptions.firstWhereOrNull(
|
final pd = policyDescriptions.firstWhereOrNull(
|
||||||
(policy) =>
|
(policy) =>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import 'package:pshared/api/responses/invitations.dart';
|
|
||||||
import 'package:pshared/data/mapper/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/service/services.dart';
|
|
||||||
import 'package:pshared/service/template.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationService {
|
|
||||||
static const String _objectType = Services.invitations;
|
|
||||||
|
|
||||||
static final BasicService<Invitation> _basicService = BasicService<Invitation>(
|
|
||||||
objectType: _objectType,
|
|
||||||
fromJson: (json) => InvitationsResponse.fromJson(json).invitations.map((dto) => dto.toDomain()).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
static BasicService<Invitation> get basicService => _basicService;
|
|
||||||
|
|
||||||
static Future<List<Invitation>> list(String organizationRef, String parentRef) {
|
|
||||||
return _basicService.list(organizationRef, parentRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Invitation>> create(String organizationRef, Invitation invitation) {
|
|
||||||
return _basicService.create(organizationRef, invitation.toDTO().toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Invitation>> update(Invitation invitation) {
|
|
||||||
return _basicService.update(invitation.toDTO().toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Invitation>> delete(Invitation invitation) {
|
|
||||||
return _basicService.delete(invitation.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Invitation>> archive({
|
|
||||||
required String organizationRef,
|
|
||||||
required Invitation invitation,
|
|
||||||
required bool archived,
|
|
||||||
}) {
|
|
||||||
return _basicService.archive(
|
|
||||||
organizationRef: organizationRef,
|
|
||||||
objectRef: invitation.id,
|
|
||||||
newIsArchived: archived,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ class PayoutRoutes {
|
|||||||
static const dashboard = 'dashboard';
|
static const dashboard = 'dashboard';
|
||||||
static const sendPayout = payment;
|
static const sendPayout = payment;
|
||||||
static const recipients = 'payout-recipients';
|
static const recipients = 'payout-recipients';
|
||||||
static const invitations = 'payout-invitations';
|
|
||||||
static const addRecipient = 'payout-add-recipient';
|
static const addRecipient = 'payout-add-recipient';
|
||||||
static const payment = 'payout-payment';
|
static const payment = 'payout-payment';
|
||||||
static const settings = 'payout-settings';
|
static const settings = 'payout-settings';
|
||||||
@@ -27,7 +26,6 @@ class PayoutRoutes {
|
|||||||
|
|
||||||
static const dashboardPath = '/dashboard';
|
static const dashboardPath = '/dashboard';
|
||||||
static const recipientsPath = '/dashboard/recipients';
|
static const recipientsPath = '/dashboard/recipients';
|
||||||
static const invitationsPath = '/dashboard/invitations';
|
|
||||||
static const addRecipientPath = '/dashboard/recipients/add';
|
static const addRecipientPath = '/dashboard/recipients/add';
|
||||||
static const paymentPath = '/dashboard/payment';
|
static const paymentPath = '/dashboard/payment';
|
||||||
static const settingsPath = '/dashboard/settings';
|
static const settingsPath = '/dashboard/settings';
|
||||||
@@ -41,16 +39,14 @@ class PayoutRoutes {
|
|||||||
case PayoutDestination.dashboard:
|
case PayoutDestination.dashboard:
|
||||||
return dashboard;
|
return dashboard;
|
||||||
case PayoutDestination.sendPayout:
|
case PayoutDestination.sendPayout:
|
||||||
return payment;
|
return payment;
|
||||||
case PayoutDestination.recipients:
|
case PayoutDestination.recipients:
|
||||||
return recipients;
|
return recipients;
|
||||||
case PayoutDestination.invitations:
|
case PayoutDestination.addrecipient:
|
||||||
return invitations;
|
return addRecipient;
|
||||||
case PayoutDestination.addrecipient:
|
case PayoutDestination.payment:
|
||||||
return addRecipient;
|
return payment;
|
||||||
case PayoutDestination.payment:
|
case PayoutDestination.settings:
|
||||||
return payment;
|
|
||||||
case PayoutDestination.settings:
|
|
||||||
return settings;
|
return settings;
|
||||||
case PayoutDestination.reports:
|
case PayoutDestination.reports:
|
||||||
return reports;
|
return reports;
|
||||||
@@ -68,16 +64,14 @@ class PayoutRoutes {
|
|||||||
case PayoutDestination.dashboard:
|
case PayoutDestination.dashboard:
|
||||||
return dashboardPath;
|
return dashboardPath;
|
||||||
case PayoutDestination.sendPayout:
|
case PayoutDestination.sendPayout:
|
||||||
return paymentPath;
|
return paymentPath;
|
||||||
case PayoutDestination.recipients:
|
case PayoutDestination.recipients:
|
||||||
return recipientsPath;
|
return recipientsPath;
|
||||||
case PayoutDestination.invitations:
|
case PayoutDestination.addrecipient:
|
||||||
return invitationsPath;
|
return addRecipientPath;
|
||||||
case PayoutDestination.addrecipient:
|
case PayoutDestination.payment:
|
||||||
return addRecipientPath;
|
return paymentPath;
|
||||||
case PayoutDestination.payment:
|
case PayoutDestination.settings:
|
||||||
return paymentPath;
|
|
||||||
case PayoutDestination.settings:
|
|
||||||
return settingsPath;
|
return settingsPath;
|
||||||
case PayoutDestination.reports:
|
case PayoutDestination.reports:
|
||||||
return reportsPath;
|
return reportsPath;
|
||||||
@@ -95,15 +89,13 @@ class PayoutRoutes {
|
|||||||
case dashboard:
|
case dashboard:
|
||||||
return PayoutDestination.dashboard;
|
return PayoutDestination.dashboard;
|
||||||
case sendPayout:
|
case sendPayout:
|
||||||
return PayoutDestination.payment;
|
return PayoutDestination.payment;
|
||||||
case recipients:
|
case recipients:
|
||||||
return PayoutDestination.recipients;
|
return PayoutDestination.recipients;
|
||||||
case invitations:
|
case addRecipient:
|
||||||
return PayoutDestination.invitations;
|
return PayoutDestination.addrecipient;
|
||||||
case addRecipient:
|
case settings:
|
||||||
return PayoutDestination.addrecipient;
|
return PayoutDestination.settings;
|
||||||
case settings:
|
|
||||||
return PayoutDestination.settings;
|
|
||||||
case reports:
|
case reports:
|
||||||
return PayoutDestination.reports;
|
return PayoutDestination.reports;
|
||||||
case methods:
|
case methods:
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
import 'package:pshared/models/payment/type.dart';
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/payment/amount.dart';
|
||||||
|
import 'package:pshared/provider/payment/flow.dart';
|
||||||
|
import 'package:pshared/provider/payment/provider.dart';
|
||||||
|
import 'package:pshared/provider/payment/quotation.dart';
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/router/pages.dart';
|
import 'package:pweb/app/router/pages.dart';
|
||||||
import 'package:pweb/app/router/payout_routes.dart';
|
import 'package:pweb/app/router/payout_routes.dart';
|
||||||
@@ -14,7 +20,6 @@ import 'package:pshared/models/payment/wallet.dart';
|
|||||||
import 'package:pweb/pages/address_book/form/page.dart';
|
import 'package:pweb/pages/address_book/form/page.dart';
|
||||||
import 'package:pweb/pages/address_book/page/page.dart';
|
import 'package:pweb/pages/address_book/page/page.dart';
|
||||||
import 'package:pweb/pages/dashboard/dashboard.dart';
|
import 'package:pweb/pages/dashboard/dashboard.dart';
|
||||||
import 'package:pweb/pages/invitations/page.dart';
|
|
||||||
import 'package:pweb/pages/payment_methods/page.dart';
|
import 'package:pweb/pages/payment_methods/page.dart';
|
||||||
import 'package:pweb/pages/payout_page/page.dart';
|
import 'package:pweb/pages/payout_page/page.dart';
|
||||||
import 'package:pweb/pages/payout_page/wallet/edit/page.dart';
|
import 'package:pweb/pages/payout_page/wallet/edit/page.dart';
|
||||||
@@ -30,10 +35,40 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
|
|
||||||
RouteBase payoutShellRoute() => ShellRoute(
|
RouteBase payoutShellRoute() => ShellRoute(
|
||||||
builder: (context, state, child) => PageSelector(
|
builder: (context, state, child) => MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProxyProvider2<OrganizationsProvider, RecipientsProvider, PaymentMethodsProvider>(
|
||||||
|
create: (_) => PaymentMethodsProvider(),
|
||||||
|
update: (context, organizations, recipients, provider) => provider!..updateProviders(organizations, recipients),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider2<RecipientsProvider, PaymentMethodsProvider, PaymentFlowProvider>(
|
||||||
|
create: (_) => PaymentFlowProvider(initialType: PaymentType.bankAccount),
|
||||||
|
update: (context, recipients, methods, provider) => provider!..update(
|
||||||
|
recipients,
|
||||||
|
methods,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (_) => PaymentAmountProvider(),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider6<OrganizationsProvider, PaymentAmountProvider, WalletsProvider, PaymentFlowProvider, RecipientsProvider, PaymentMethodsProvider, QuotationProvider>(
|
||||||
|
create: (_) => QuotationProvider(),
|
||||||
|
update: (_, organization, payment, wallet, flow, recipients, methods, provider) =>
|
||||||
|
provider!..update(organization, payment, wallet, flow, recipients, methods),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider2<OrganizationsProvider, QuotationProvider, PaymentProvider>(
|
||||||
|
create: (_) => PaymentProvider(),
|
||||||
|
update: (context, organization, quotation, provider) => provider!..update(
|
||||||
|
organization,
|
||||||
|
quotation,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: PageSelector(
|
||||||
child: child,
|
child: child,
|
||||||
routerState: state,
|
routerState: state,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: PayoutRoutes.dashboard,
|
name: PayoutRoutes.dashboard,
|
||||||
@@ -87,13 +122,6 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: PayoutRoutes.invitations,
|
|
||||||
path: PayoutRoutes.invitationsPath,
|
|
||||||
pageBuilder: (_, __) => const NoTransitionPage(
|
|
||||||
child: InvitationsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: PayoutRoutes.addRecipient,
|
name: PayoutRoutes.addRecipient,
|
||||||
path: PayoutRoutes.addRecipientPath,
|
path: PayoutRoutes.addRecipientPath,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"errorVerificationTokenNotFound": "Account for verification not found. Sign up again",
|
"errorVerificationTokenNotFound": "Account for verification not found. Sign up again",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"edited": "Edited",
|
"edited": "Edited",
|
||||||
"errorDataConflict": "This action conflicts with existing data. Check for duplicates or conflicting values and try again.",
|
"errorDataConflict": "We can’t process your data because it has conflicting or contradictory information.",
|
||||||
"errorAccessDenied": "You do not have permission to access this resource. If you need access, please contact an administrator.",
|
"errorAccessDenied": "You do not have permission to access this resource. If you need access, please contact an administrator.",
|
||||||
"errorBrokenPayload": "The data you sent is invalid or incomplete. Please check your submission and try again.",
|
"errorBrokenPayload": "The data you sent is invalid or incomplete. Please check your submission and try again.",
|
||||||
"errorInvalidArgument": "One or more arguments are invalid. Verify your input and try again.",
|
"errorInvalidArgument": "One or more arguments are invalid. Verify your input and try again.",
|
||||||
@@ -66,7 +66,6 @@
|
|||||||
"showDetailsAction": "Show Details",
|
"showDetailsAction": "Show Details",
|
||||||
"errorLogin": "Error logging in",
|
"errorLogin": "Error logging in",
|
||||||
"errorCreatingInvitation": "Failed to create invitaiton",
|
"errorCreatingInvitation": "Failed to create invitaiton",
|
||||||
"errorLoadingInvitations": "Failed to load invitations",
|
|
||||||
"@errorCreatingInvitation": {
|
"@errorCreatingInvitation": {
|
||||||
"description": "Error message displayed when invitation creation fails"
|
"description": "Error message displayed when invitation creation fails"
|
||||||
},
|
},
|
||||||
@@ -94,7 +93,6 @@
|
|||||||
"payoutNavDashboard": "Dashboard",
|
"payoutNavDashboard": "Dashboard",
|
||||||
"payoutNavSendPayout": "Send payout",
|
"payoutNavSendPayout": "Send payout",
|
||||||
"payoutNavRecipients": "Recipients",
|
"payoutNavRecipients": "Recipients",
|
||||||
"payoutNavInvitations": "Invitations",
|
|
||||||
"payoutNavReports": "Reports",
|
"payoutNavReports": "Reports",
|
||||||
"payoutNavSettings": "Settings",
|
"payoutNavSettings": "Settings",
|
||||||
"payoutNavLogout": "Logout",
|
"payoutNavLogout": "Logout",
|
||||||
@@ -187,47 +185,6 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"invitationsTitle": "Invite your teammates",
|
|
||||||
"invitationsSubtitle": "Send invitations for restricted employee accounts and see their status in one place.",
|
|
||||||
"invitationCreateTitle": "New invitation",
|
|
||||||
"invitationEmailLabel": "Work email",
|
|
||||||
"invitationNameLabel": "Full name",
|
|
||||||
"invitationRoleLabel": "Role",
|
|
||||||
"invitationMessageLabel": "Message (optional)",
|
|
||||||
"invitationExpiresIn": "Expires in {days} days",
|
|
||||||
"@invitationExpiresIn": {
|
|
||||||
"placeholders": {
|
|
||||||
"days": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"invitationSendButton": "Send invitation",
|
|
||||||
"invitationCreatedSuccess": "Invitation sent",
|
|
||||||
"invitationSearchHint": "Search invitations",
|
|
||||||
"invitationFilterAll": "All",
|
|
||||||
"invitationFilterPending": "Pending",
|
|
||||||
"invitationFilterAccepted": "Accepted",
|
|
||||||
"invitationFilterDeclined": "Declined",
|
|
||||||
"invitationFilterRevoked": "Revoked",
|
|
||||||
"invitationFilterExpired": "Expired",
|
|
||||||
"invitationFilterArchived": "Archived",
|
|
||||||
"invitationListEmpty": "No invitations yet",
|
|
||||||
"invitationStatusPending": "Pending",
|
|
||||||
"invitationStatusAccepted": "Accepted",
|
|
||||||
"invitationStatusDeclined": "Declined",
|
|
||||||
"invitationStatusRevoked": "Revoked",
|
|
||||||
"invitationStatusExpired": "Expired",
|
|
||||||
"invitationExpires": "Expires {date}",
|
|
||||||
"invitationExpired": "Expired {date}",
|
|
||||||
"invitationInvitedBy": "Invited by",
|
|
||||||
"invitationArchiveAction": "Archive",
|
|
||||||
"invitationRevokeAction": "Revoke",
|
|
||||||
"invitationArchived": "Invitation archived",
|
|
||||||
"invitationRevoked": "Invitation revoked",
|
|
||||||
"invitationArchiveFailed": "Could not archive the invitation",
|
|
||||||
"invitationRevokeFailed": "Could not revoke the invitation",
|
|
||||||
"invitationUnknownRole": "Unknown role",
|
|
||||||
|
|
||||||
"operationfryTitle": "Operation history",
|
"operationfryTitle": "Operation history",
|
||||||
"@operationfryTitle": {
|
"@operationfryTitle": {
|
||||||
@@ -403,7 +360,6 @@
|
|||||||
"enterBik": "Enter BIK",
|
"enterBik": "Enter BIK",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"expiryDate": "Expiry (MM/YY)",
|
"expiryDate": "Expiry (MM/YY)",
|
||||||
"enterExpiryDate": "Enter card expiry date",
|
|
||||||
"firstName": "First Name",
|
"firstName": "First Name",
|
||||||
"enterFirstName": "Enter First Name",
|
"enterFirstName": "Enter First Name",
|
||||||
"lastName": "Last Name",
|
"lastName": "Last Name",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова",
|
"errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова",
|
||||||
"created": "Создано",
|
"created": "Создано",
|
||||||
"edited": "Изменено",
|
"edited": "Изменено",
|
||||||
"errorDataConflict": "Действие конфликтует с уже существующими данными. Проверьте дубликаты или противоречащие значения и попробуйте снова.",
|
"errorDataConflict": "Мы не можем обработать ваши данные, так как они содержат конфликтующую или противоречивую информацию.",
|
||||||
"errorAccessDenied": "У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.",
|
"errorAccessDenied": "У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.",
|
||||||
"errorBrokenPayload": "Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.",
|
"errorBrokenPayload": "Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.",
|
||||||
"errorInvalidArgument": "Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.",
|
"errorInvalidArgument": "Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.",
|
||||||
@@ -66,7 +66,6 @@
|
|||||||
"showDetailsAction": "Показать детали",
|
"showDetailsAction": "Показать детали",
|
||||||
"errorLogin": "Ошибка входа",
|
"errorLogin": "Ошибка входа",
|
||||||
"errorCreatingInvitation": "Не удалось создать приглашение",
|
"errorCreatingInvitation": "Не удалось создать приглашение",
|
||||||
"errorLoadingInvitations": "Не удалось загрузить приглашения",
|
|
||||||
"@errorCreatingInvitation": {
|
"@errorCreatingInvitation": {
|
||||||
"description": "Сообщение об ошибке, отображаемое при неудачном создании приглашения"
|
"description": "Сообщение об ошибке, отображаемое при неудачном создании приглашения"
|
||||||
},
|
},
|
||||||
@@ -94,7 +93,6 @@
|
|||||||
"payoutNavDashboard": "Дашборд",
|
"payoutNavDashboard": "Дашборд",
|
||||||
"payoutNavSendPayout": "Отправить выплату",
|
"payoutNavSendPayout": "Отправить выплату",
|
||||||
"payoutNavRecipients": "Получатели",
|
"payoutNavRecipients": "Получатели",
|
||||||
"payoutNavInvitations": "Приглашения",
|
|
||||||
"payoutNavReports": "Отчеты",
|
"payoutNavReports": "Отчеты",
|
||||||
"payoutNavSettings": "Настройки",
|
"payoutNavSettings": "Настройки",
|
||||||
"payoutNavLogout": "Выйти",
|
"payoutNavLogout": "Выйти",
|
||||||
@@ -187,47 +185,6 @@
|
|||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"confirm": "Подтвердить",
|
"confirm": "Подтвердить",
|
||||||
"back": "Назад",
|
"back": "Назад",
|
||||||
"invitationsTitle": "Пригласите сотрудников",
|
|
||||||
"invitationsSubtitle": "Отправляйте приглашения сотрудникам с ограниченными аккаунтами и отслеживайте статусы.",
|
|
||||||
"invitationCreateTitle": "Новое приглашение",
|
|
||||||
"invitationEmailLabel": "Рабочий email",
|
|
||||||
"invitationNameLabel": "Полное имя",
|
|
||||||
"invitationRoleLabel": "Роль",
|
|
||||||
"invitationMessageLabel": "Сообщение (необязательно)",
|
|
||||||
"invitationExpiresIn": "Истекает через {days} дн.",
|
|
||||||
"@invitationExpiresIn": {
|
|
||||||
"placeholders": {
|
|
||||||
"days": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"invitationSendButton": "Отправить приглашение",
|
|
||||||
"invitationCreatedSuccess": "Приглашение отправлено",
|
|
||||||
"invitationSearchHint": "Поиск приглашений",
|
|
||||||
"invitationFilterAll": "Все",
|
|
||||||
"invitationFilterPending": "В ожидании",
|
|
||||||
"invitationFilterAccepted": "Принятые",
|
|
||||||
"invitationFilterDeclined": "Отклоненные",
|
|
||||||
"invitationFilterRevoked": "Отозванные",
|
|
||||||
"invitationFilterExpired": "Истекшие",
|
|
||||||
"invitationFilterArchived": "Архив",
|
|
||||||
"invitationListEmpty": "Пока нет приглашений",
|
|
||||||
"invitationStatusPending": "В ожидании",
|
|
||||||
"invitationStatusAccepted": "Принято",
|
|
||||||
"invitationStatusDeclined": "Отклонено",
|
|
||||||
"invitationStatusRevoked": "Отозвано",
|
|
||||||
"invitationStatusExpired": "Истекло",
|
|
||||||
"invitationExpires": "Истекает {date}",
|
|
||||||
"invitationExpired": "Истекло {date}",
|
|
||||||
"invitationInvitedBy": "Пригласил",
|
|
||||||
"invitationArchiveAction": "Архивировать",
|
|
||||||
"invitationRevokeAction": "Отозвать",
|
|
||||||
"invitationArchived": "Приглашение архивировано",
|
|
||||||
"invitationRevoked": "Приглашение отозвано",
|
|
||||||
"invitationArchiveFailed": "Не удалось архивировать приглашение",
|
|
||||||
"invitationRevokeFailed": "Не удалось отозвать приглашение",
|
|
||||||
"invitationUnknownRole": "Неизвестная роль",
|
|
||||||
|
|
||||||
"operationfryTitle": "История операций",
|
"operationfryTitle": "История операций",
|
||||||
"@operationfryTitle": {
|
"@operationfryTitle": {
|
||||||
@@ -403,7 +360,6 @@
|
|||||||
"enterBik": "Введите БИК",
|
"enterBik": "Введите БИК",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"expiryDate": "Срок действия (ММ/ГГ)",
|
"expiryDate": "Срок действия (ММ/ГГ)",
|
||||||
"enterExpiryDate": "Введите срок действия карты",
|
|
||||||
"firstName": "Имя",
|
"firstName": "Имя",
|
||||||
"enterFirstName": "Введите имя",
|
"enterFirstName": "Введите имя",
|
||||||
"lastName": "Фамилия",
|
"lastName": "Фамилия",
|
||||||
|
|||||||
@@ -13,15 +13,8 @@ import 'package:pshared/provider/permissions.dart';
|
|||||||
import 'package:pshared/provider/account.dart';
|
import 'package:pshared/provider/account.dart';
|
||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
import 'package:pshared/provider/accounts/employees.dart';
|
import 'package:pshared/provider/accounts/employees.dart';
|
||||||
import 'package:pshared/provider/payment/amount.dart';
|
|
||||||
import 'package:pshared/provider/payment/flow.dart';
|
|
||||||
import 'package:pshared/provider/payment/provider.dart';
|
|
||||||
import 'package:pshared/provider/payment/quotation.dart';
|
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
|
||||||
import 'package:pshared/provider/payment/wallets.dart';
|
import 'package:pshared/provider/payment/wallets.dart';
|
||||||
import 'package:pshared/provider/invitations.dart';
|
|
||||||
import 'package:pshared/models/payment/type.dart';
|
|
||||||
import 'package:pshared/service/payment/wallets.dart';
|
import 'package:pshared/service/payment/wallets.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/app.dart';
|
import 'package:pweb/app/app.dart';
|
||||||
@@ -87,14 +80,6 @@ void main() async {
|
|||||||
create: (_) => RecipientsProvider(),
|
create: (_) => RecipientsProvider(),
|
||||||
update: (context, organizations, provider) => provider!..updateProviders(organizations),
|
update: (context, organizations, provider) => provider!..updateProviders(organizations),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider<OrganizationsProvider, InvitationsProvider>(
|
|
||||||
create: (_) => InvitationsProvider(),
|
|
||||||
update: (context, organizations, provider) => provider!..updateProviders(organizations),
|
|
||||||
),
|
|
||||||
ChangeNotifierProxyProvider2<OrganizationsProvider, RecipientsProvider, PaymentMethodsProvider>(
|
|
||||||
create: (_) => PaymentMethodsProvider(),
|
|
||||||
update: (context, organizations, recipients, provider) => provider!..updateProviders(organizations, recipients),
|
|
||||||
),
|
|
||||||
ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>(
|
ChangeNotifierProxyProvider<OrganizationsProvider, WalletsProvider>(
|
||||||
create: (_) => WalletsProvider(ApiWalletsService()),
|
create: (_) => WalletsProvider(ApiWalletsService()),
|
||||||
update: (context, organizations, provider) => provider!..update(organizations),
|
update: (context, organizations, provider) => provider!..update(organizations),
|
||||||
@@ -102,32 +87,9 @@ void main() async {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(),
|
create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider2<RecipientsProvider, PaymentMethodsProvider, PaymentFlowProvider>(
|
|
||||||
create: (_) => PaymentFlowProvider(initialType: PaymentType.bankAccount),
|
|
||||||
update: (context, recipients, methods, provider) => provider!..update(
|
|
||||||
recipients,
|
|
||||||
methods,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
|
||||||
create: (_) => PaymentAmountProvider(),
|
|
||||||
),
|
|
||||||
ChangeNotifierProxyProvider6<OrganizationsProvider, PaymentAmountProvider, WalletsProvider, PaymentFlowProvider, RecipientsProvider, PaymentMethodsProvider, QuotationProvider>(
|
|
||||||
create: (_) => QuotationProvider(),
|
|
||||||
update: (_, organization, payment, wallet, flow, recipients, methods, provider) =>
|
|
||||||
provider!..update(organization, payment, wallet, flow, recipients, methods),
|
|
||||||
),
|
|
||||||
ChangeNotifierProxyProvider2<OrganizationsProvider, QuotationProvider, PaymentProvider>(
|
|
||||||
create: (_) => PaymentProvider(),
|
|
||||||
update: (context, organization, quotation, provider) => provider!..update(
|
|
||||||
organization,
|
|
||||||
quotation,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: const PayApp(),
|
child: const PayApp(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
enum InvitationFilter {
|
|
||||||
all,
|
|
||||||
pending,
|
|
||||||
accepted,
|
|
||||||
declined,
|
|
||||||
revoked,
|
|
||||||
expired,
|
|
||||||
archived,
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/resources.dart';
|
|
||||||
import 'package:pshared/provider/account.dart';
|
|
||||||
import 'package:pshared/provider/invitations.dart';
|
|
||||||
import 'package:pshared/provider/permissions.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/header.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/form/form.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/list/list.dart';
|
|
||||||
import 'package:pweb/pages/loader.dart';
|
|
||||||
import 'package:pweb/widgets/error/snackbar.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationsPage extends StatefulWidget {
|
|
||||||
const InvitationsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<InvitationsPage> createState() => _InvitationsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InvitationsPageState extends State<InvitationsPage> {
|
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
||||||
final TextEditingController _emailController = TextEditingController();
|
|
||||||
final TextEditingController _nameController = TextEditingController();
|
|
||||||
final TextEditingController _messageController = TextEditingController();
|
|
||||||
|
|
||||||
String? _selectedRoleRef;
|
|
||||||
int _expiryDays = 7;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_bootstrapRoleSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _bootstrapRoleSelection() {
|
|
||||||
final roles = context.read<PermissionsProvider>().roleDescriptions;
|
|
||||||
if (_selectedRoleRef == null && roles.isNotEmpty) {
|
|
||||||
_selectedRoleRef = roles.first.storable.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_emailController.dispose();
|
|
||||||
_nameController.dispose();
|
|
||||||
_messageController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _sendInvitation() async {
|
|
||||||
final form = _formKey.currentState;
|
|
||||||
if (form == null || !form.validate()) return;
|
|
||||||
|
|
||||||
final account = context.read<AccountProvider>().account;
|
|
||||||
if (account == null) return;
|
|
||||||
final permissions = context.read<PermissionsProvider>();
|
|
||||||
final roleRef = _selectedRoleRef ?? permissions.roleDescriptions.firstOrNull?.storable.id;
|
|
||||||
if (roleRef == null) return;
|
|
||||||
|
|
||||||
final invitations = context.read<InvitationsProvider>();
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
|
|
||||||
await executeActionWithNotification(
|
|
||||||
context: context,
|
|
||||||
action: () => invitations.sendInvitation(
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
name: _nameController.text.trim(),
|
|
||||||
comment: _messageController.text.trim(),
|
|
||||||
roleRef: roleRef,
|
|
||||||
inviterRef: account.id,
|
|
||||||
expiresAt: DateTime.now().toUtc().add(Duration(days: _expiryDays)),
|
|
||||||
),
|
|
||||||
successMessage: loc.invitationCreatedSuccess,
|
|
||||||
errorMessage: loc.errorCreatingInvitation,
|
|
||||||
);
|
|
||||||
|
|
||||||
_emailController.clear();
|
|
||||||
_nameController.clear();
|
|
||||||
_messageController.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final permissions = context.watch<PermissionsProvider>();
|
|
||||||
|
|
||||||
if (!permissions.canRead(ResourceType.invitations)) {
|
|
||||||
return PageViewLoader(
|
|
||||||
child: Center(child: Text(loc.errorAccessDenied)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PageViewLoader(
|
|
||||||
child: SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
InvitationsHeader(loc: loc),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
InvitationsForm(
|
|
||||||
formKey: _formKey,
|
|
||||||
emailController: _emailController,
|
|
||||||
nameController: _nameController,
|
|
||||||
messageController: _messageController,
|
|
||||||
expiryDays: _expiryDays,
|
|
||||||
onExpiryChanged: (value) => setState(() => _expiryDays = value),
|
|
||||||
selectedRoleRef: _selectedRoleRef,
|
|
||||||
onRoleChanged: (role) => setState(() => _selectedRoleRef = role),
|
|
||||||
canCreate: permissions.canCreate(ResourceType.invitations),
|
|
||||||
onSubmit: _sendInvitation,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
const InvitationsList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/provider/invitations.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/widgets/error/snackbar.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationCardActions extends StatelessWidget {
|
|
||||||
final Invitation invitation;
|
|
||||||
|
|
||||||
const InvitationCardActions({super.key, required this.invitation});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (invitation.isPending && !invitation.isExpired)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () => _revokeInvitation(context),
|
|
||||||
icon: const Icon(Icons.block),
|
|
||||||
label: Text(loc.invitationRevokeAction),
|
|
||||||
),
|
|
||||||
if (!invitation.isArchived)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () => _archiveInvitation(context),
|
|
||||||
icon: const Icon(Icons.archive_outlined),
|
|
||||||
label: Text(loc.invitationArchiveAction),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _archiveInvitation(BuildContext context) async {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final provider = context.read<InvitationsProvider>();
|
|
||||||
|
|
||||||
await executeActionWithNotification(
|
|
||||||
context: context,
|
|
||||||
action: () => provider.setInvitationArchived(invitation, true),
|
|
||||||
successMessage: loc.invitationArchived,
|
|
||||||
errorMessage: loc.invitationArchiveFailed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _revokeInvitation(BuildContext context) async {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final provider = context.read<InvitationsProvider>();
|
|
||||||
|
|
||||||
await executeActionWithNotification(
|
|
||||||
context: context,
|
|
||||||
action: () => provider.revokeInvitation(invitation),
|
|
||||||
successMessage: loc.invitationRevoked,
|
|
||||||
errorMessage: loc.invitationRevokeFailed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/view.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationsCard extends StatelessWidget {
|
|
||||||
final Invitation invitation;
|
|
||||||
|
|
||||||
const InvitationsCard({super.key, required this.invitation});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => InvitationCardView(invitation: invitation);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/helpers.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationCardDetails extends StatelessWidget {
|
|
||||||
final Invitation invitation;
|
|
||||||
final String roleLabel;
|
|
||||||
final String inviterName;
|
|
||||||
final DateFormat dateFormat;
|
|
||||||
final AppLocalizations loc;
|
|
||||||
|
|
||||||
const InvitationCardDetails({
|
|
||||||
super.key,
|
|
||||||
required this.invitation,
|
|
||||||
required this.roleLabel,
|
|
||||||
required this.inviterName,
|
|
||||||
required this.dateFormat,
|
|
||||||
required this.loc,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Wrap(
|
|
||||||
spacing: 20,
|
|
||||||
runSpacing: 10,
|
|
||||||
children: [
|
|
||||||
InvitationInfoRow(
|
|
||||||
icon: Icons.badge_outlined,
|
|
||||||
label: loc.invitationRoleLabel,
|
|
||||||
value: roleLabel,
|
|
||||||
),
|
|
||||||
InvitationInfoRow(
|
|
||||||
icon: Icons.schedule_outlined,
|
|
||||||
label: invitation.isExpired
|
|
||||||
? loc.invitationExpired(dateFormat.format(invitation.expiresAt.toLocal()))
|
|
||||||
: loc.invitationExpires(dateFormat.format(invitation.expiresAt.toLocal())),
|
|
||||||
value: '',
|
|
||||||
),
|
|
||||||
InvitationInfoRow(
|
|
||||||
icon: Icons.person_outline,
|
|
||||||
label: loc.invitationInvitedBy,
|
|
||||||
value: inviterName,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (invitation.content.comment.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
invitation.content.comment,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationCardHeader extends StatelessWidget {
|
|
||||||
final Invitation invitation;
|
|
||||||
final String statusLabel;
|
|
||||||
final Color statusColor;
|
|
||||||
|
|
||||||
const InvitationCardHeader({
|
|
||||||
super.key,
|
|
||||||
required this.invitation,
|
|
||||||
required this.statusLabel,
|
|
||||||
required this.statusColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
invitation.inviteeDisplayName,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
invitation.content.email,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Chip(
|
|
||||||
backgroundColor: statusColor.withAlpha(40),
|
|
||||||
label: Text(
|
|
||||||
statusLabel,
|
|
||||||
style: TextStyle(color: statusColor, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/status.dart';
|
|
||||||
import 'package:pshared/models/permissions/descriptions/role.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationInfoRow extends StatelessWidget {
|
|
||||||
final IconData icon;
|
|
||||||
final String label;
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
const InvitationInfoRow({
|
|
||||||
super.key,
|
|
||||||
required this.icon,
|
|
||||||
required this.label,
|
|
||||||
required this.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 18, color: theme.hintColor),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
if (value.isEmpty)
|
|
||||||
Text(label, style: theme.textTheme.bodyMedium)
|
|
||||||
else
|
|
||||||
RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
children: [
|
|
||||||
TextSpan(text: '$label: ', style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
TextSpan(text: value),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String invitationStatusLabel(Invitation invitation, AppLocalizations loc) {
|
|
||||||
if (invitation.isExpired && invitation.isPending) {
|
|
||||||
return loc.invitationStatusExpired;
|
|
||||||
}
|
|
||||||
switch (invitation.status) {
|
|
||||||
case InvitationStatus.created:
|
|
||||||
case InvitationStatus.sent:
|
|
||||||
return loc.invitationStatusPending;
|
|
||||||
case InvitationStatus.accepted:
|
|
||||||
return loc.invitationStatusAccepted;
|
|
||||||
case InvitationStatus.declined:
|
|
||||||
return loc.invitationStatusDeclined;
|
|
||||||
case InvitationStatus.revoked:
|
|
||||||
return loc.invitationStatusRevoked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color invitationStatusColor(ThemeData theme, Invitation invitation) {
|
|
||||||
if (invitation.isExpired && invitation.isPending) {
|
|
||||||
return theme.disabledColor;
|
|
||||||
}
|
|
||||||
switch (invitation.status) {
|
|
||||||
case InvitationStatus.created:
|
|
||||||
case InvitationStatus.sent:
|
|
||||||
return Colors.amber.shade800;
|
|
||||||
case InvitationStatus.accepted:
|
|
||||||
return Colors.green.shade700;
|
|
||||||
case InvitationStatus.declined:
|
|
||||||
case InvitationStatus.revoked:
|
|
||||||
return Colors.red.shade700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String invitationRoleLabel(List<RoleDescription> roles, Invitation invitation, AppLocalizations loc) {
|
|
||||||
final role = roles.firstWhereOrNull((r) => r.storable.id == invitation.roleRef);
|
|
||||||
return role?.describable.name ?? loc.invitationUnknownRole;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/resources.dart';
|
|
||||||
import 'package:pshared/provider/accounts/employees.dart';
|
|
||||||
import 'package:pshared/provider/permissions.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/actions.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/details.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/header.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/helpers.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationCardView extends StatelessWidget {
|
|
||||||
final Invitation invitation;
|
|
||||||
|
|
||||||
const InvitationCardView({super.key, required this.invitation});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final permissions = context.watch<PermissionsProvider>();
|
|
||||||
final employees = context.watch<EmployeesProvider>();
|
|
||||||
final dateFormat = DateFormat.yMMMd().add_Hm();
|
|
||||||
|
|
||||||
final statusLabel = invitationStatusLabel(invitation, loc);
|
|
||||||
final statusColor = invitationStatusColor(theme, invitation);
|
|
||||||
final roleLabel = invitationRoleLabel(permissions.roleDescriptions, invitation, loc);
|
|
||||||
final inviterName = employees.getEmployee(invitation.inviterRef)?.fullName ?? loc.unknown;
|
|
||||||
final canUpdate = permissions.canUpdate(ResourceType.invitations);
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
side: BorderSide(color: theme.dividerColor.withAlpha(20)),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
InvitationCardHeader(
|
|
||||||
invitation: invitation,
|
|
||||||
statusLabel: statusLabel,
|
|
||||||
statusColor: statusColor,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
InvitationCardDetails(
|
|
||||||
invitation: invitation,
|
|
||||||
roleLabel: roleLabel,
|
|
||||||
inviterName: inviterName,
|
|
||||||
dateFormat: dateFormat,
|
|
||||||
loc: loc,
|
|
||||||
),
|
|
||||||
if (canUpdate) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
InvitationCardActions(invitation: invitation),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/models/invitation_filter.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/filter/invitation_filter.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationFilterChips extends StatelessWidget {
|
|
||||||
final InvitationFilter selectedFilter;
|
|
||||||
final ValueChanged<InvitationFilter> onSelected;
|
|
||||||
|
|
||||||
const InvitationFilterChips({
|
|
||||||
super.key,
|
|
||||||
required this.selectedFilter,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
return Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: InvitationFilter.values.map(
|
|
||||||
(filter) => ChoiceChip(
|
|
||||||
label: Text(invitationFilterLabel(filter, loc)),
|
|
||||||
selected: selectedFilter == filter,
|
|
||||||
onSelected: (_) => onSelected(filter),
|
|
||||||
),
|
|
||||||
).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/models/invitation/status.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/models/invitation_filter.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
String invitationFilterLabel(InvitationFilter filter, AppLocalizations loc) {
|
|
||||||
switch (filter) {
|
|
||||||
case InvitationFilter.all:
|
|
||||||
return loc.invitationFilterAll;
|
|
||||||
case InvitationFilter.pending:
|
|
||||||
return loc.invitationFilterPending;
|
|
||||||
case InvitationFilter.accepted:
|
|
||||||
return loc.invitationFilterAccepted;
|
|
||||||
case InvitationFilter.declined:
|
|
||||||
return loc.invitationFilterDeclined;
|
|
||||||
case InvitationFilter.revoked:
|
|
||||||
return loc.invitationFilterRevoked;
|
|
||||||
case InvitationFilter.expired:
|
|
||||||
return loc.invitationFilterExpired;
|
|
||||||
case InvitationFilter.archived:
|
|
||||||
return loc.invitationFilterArchived;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool invitationFilterMatches(InvitationFilter filter, Invitation inv) {
|
|
||||||
switch (filter) {
|
|
||||||
case InvitationFilter.pending:
|
|
||||||
return inv.isPending && !inv.isExpired;
|
|
||||||
case InvitationFilter.accepted:
|
|
||||||
return inv.status == InvitationStatus.accepted;
|
|
||||||
case InvitationFilter.declined:
|
|
||||||
return inv.status == InvitationStatus.declined;
|
|
||||||
case InvitationFilter.revoked:
|
|
||||||
return inv.status == InvitationStatus.revoked;
|
|
||||||
case InvitationFilter.expired:
|
|
||||||
return inv.isExpired && inv.isPending;
|
|
||||||
case InvitationFilter.archived:
|
|
||||||
return inv.isArchived;
|
|
||||||
case InvitationFilter.all:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationFormActions extends StatelessWidget {
|
|
||||||
final int expiryDays;
|
|
||||||
final ValueChanged<int> onExpiryChanged;
|
|
||||||
final bool canCreate;
|
|
||||||
final bool hasRoles;
|
|
||||||
final VoidCallback onSubmit;
|
|
||||||
|
|
||||||
const InvitationFormActions({
|
|
||||||
super.key,
|
|
||||||
required this.expiryDays,
|
|
||||||
required this.onExpiryChanged,
|
|
||||||
required this.canCreate,
|
|
||||||
required this.hasRoles,
|
|
||||||
required this.onSubmit,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Text(loc.invitationExpiresIn(expiryDays)),
|
|
||||||
Expanded(
|
|
||||||
child: Slider(
|
|
||||||
label: '$expiryDays',
|
|
||||||
min: 1,
|
|
||||||
max: 30,
|
|
||||||
value: expiryDays.toDouble(),
|
|
||||||
onChanged: (value) => onExpiryChanged(value.round()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: canCreate && hasRoles ? onSubmit : null,
|
|
||||||
icon: const Icon(Icons.send_outlined),
|
|
||||||
label: Text(loc.invitationSendButton),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/permissions/descriptions/role.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationFormFields extends StatelessWidget {
|
|
||||||
final List<RoleDescription> roles;
|
|
||||||
final TextEditingController emailController;
|
|
||||||
final TextEditingController nameController;
|
|
||||||
final TextEditingController messageController;
|
|
||||||
final String? selectedRoleRef;
|
|
||||||
final ValueChanged<String?> onRoleChanged;
|
|
||||||
|
|
||||||
const InvitationFormFields({
|
|
||||||
super.key,
|
|
||||||
required this.roles,
|
|
||||||
required this.emailController,
|
|
||||||
required this.nameController,
|
|
||||||
required this.messageController,
|
|
||||||
required this.selectedRoleRef,
|
|
||||||
required this.onRoleChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 320,
|
|
||||||
child: TextFormField(
|
|
||||||
controller: emailController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: loc.invitationEmailLabel,
|
|
||||||
prefixIcon: const Icon(Icons.alternate_email_outlined),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
validator: (value) => (value == null || value.trim().isEmpty)
|
|
||||||
? loc.errorEmailMissing
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 280,
|
|
||||||
child: TextFormField(
|
|
||||||
controller: nameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: loc.invitationNameLabel,
|
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 260,
|
|
||||||
child: DropdownButtonFormField<String>(
|
|
||||||
initialValue: selectedRoleRef ?? (roles.isNotEmpty ? roles.first.storable.id : null),
|
|
||||||
items: roles.map((role) => DropdownMenuItem(
|
|
||||||
value: role.storable.id,
|
|
||||||
child: Text(role.describable.name),
|
|
||||||
)).toList(),
|
|
||||||
onChanged: roles.isEmpty ? null : onRoleChanged,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: loc.invitationRoleLabel,
|
|
||||||
prefixIcon: const Icon(Icons.security_outlined),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextFormField(
|
|
||||||
controller: messageController,
|
|
||||||
minLines: 2,
|
|
||||||
maxLines: 3,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: loc.invitationMessageLabel,
|
|
||||||
prefixIcon: const Icon(Icons.chat_bubble_outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/form/view.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationsForm extends StatelessWidget {
|
|
||||||
final GlobalKey<FormState> formKey;
|
|
||||||
final TextEditingController emailController;
|
|
||||||
final TextEditingController nameController;
|
|
||||||
final TextEditingController messageController;
|
|
||||||
final int expiryDays;
|
|
||||||
final ValueChanged<int> onExpiryChanged;
|
|
||||||
final String? selectedRoleRef;
|
|
||||||
final ValueChanged<String?> onRoleChanged;
|
|
||||||
final bool canCreate;
|
|
||||||
final VoidCallback onSubmit;
|
|
||||||
|
|
||||||
const InvitationsForm({
|
|
||||||
super.key,
|
|
||||||
required this.formKey,
|
|
||||||
required this.emailController,
|
|
||||||
required this.nameController,
|
|
||||||
required this.messageController,
|
|
||||||
required this.expiryDays,
|
|
||||||
required this.onExpiryChanged,
|
|
||||||
required this.selectedRoleRef,
|
|
||||||
required this.onRoleChanged,
|
|
||||||
required this.canCreate,
|
|
||||||
required this.onSubmit,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => InvitationFormView(
|
|
||||||
formKey: formKey,
|
|
||||||
emailController: emailController,
|
|
||||||
nameController: nameController,
|
|
||||||
messageController: messageController,
|
|
||||||
expiryDays: expiryDays,
|
|
||||||
onExpiryChanged: onExpiryChanged,
|
|
||||||
selectedRoleRef: selectedRoleRef,
|
|
||||||
onRoleChanged: onRoleChanged,
|
|
||||||
canCreate: canCreate,
|
|
||||||
onSubmit: onSubmit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/provider/permissions.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/form/actions.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/form/fields.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationFormView extends StatelessWidget {
|
|
||||||
final GlobalKey<FormState> formKey;
|
|
||||||
final TextEditingController emailController;
|
|
||||||
final TextEditingController nameController;
|
|
||||||
final TextEditingController messageController;
|
|
||||||
final int expiryDays;
|
|
||||||
final ValueChanged<int> onExpiryChanged;
|
|
||||||
final String? selectedRoleRef;
|
|
||||||
final ValueChanged<String?> onRoleChanged;
|
|
||||||
final bool canCreate;
|
|
||||||
final VoidCallback onSubmit;
|
|
||||||
|
|
||||||
const InvitationFormView({
|
|
||||||
super.key,
|
|
||||||
required this.formKey,
|
|
||||||
required this.emailController,
|
|
||||||
required this.nameController,
|
|
||||||
required this.messageController,
|
|
||||||
required this.expiryDays,
|
|
||||||
required this.onExpiryChanged,
|
|
||||||
required this.selectedRoleRef,
|
|
||||||
required this.onRoleChanged,
|
|
||||||
required this.canCreate,
|
|
||||||
required this.onSubmit,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final roles = context.watch<PermissionsProvider>().roleDescriptions;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: theme.colorScheme.surfaceContainerHighest.withAlpha(40),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Form(
|
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
loc.invitationCreateTitle,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
InvitationFormFields(
|
|
||||||
roles: roles,
|
|
||||||
emailController: emailController,
|
|
||||||
nameController: nameController,
|
|
||||||
messageController: messageController,
|
|
||||||
selectedRoleRef: selectedRoleRef,
|
|
||||||
onRoleChanged: onRoleChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
InvitationFormActions(
|
|
||||||
expiryDays: expiryDays,
|
|
||||||
onExpiryChanged: onExpiryChanged,
|
|
||||||
canCreate: canCreate,
|
|
||||||
hasRoles: roles.isNotEmpty,
|
|
||||||
onSubmit: onSubmit,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationsHeader extends StatelessWidget {
|
|
||||||
final AppLocalizations loc;
|
|
||||||
|
|
||||||
const InvitationsHeader({super.key, required this.loc});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
loc.invitationsTitle,
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
loc.invitationsSubtitle,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).hintColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/card/card.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationListBody extends StatelessWidget {
|
|
||||||
final List<Invitation> invitations;
|
|
||||||
|
|
||||||
const InvitationListBody({super.key, required this.invitations});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
|
|
||||||
if (invitations.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
|
||||||
child: Text(loc.invitationListEmpty),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: invitations.length,
|
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
||||||
itemBuilder: (_, index) => InvitationsCard(invitation: invitations[index]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/pages/invitations/widgets/list/view.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationsList extends StatefulWidget {
|
|
||||||
const InvitationsList({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<InvitationsList> createState() => _InvitationsListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InvitationsListState extends State<InvitationsList> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => const InvitationListView();
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/models/invitation/invitation.dart';
|
|
||||||
import 'package:pshared/provider/invitations.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/models/invitation_filter.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/filter/invitation_filter.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/filter/chips.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/list/body.dart';
|
|
||||||
import 'package:pweb/pages/invitations/widgets/search_field.dart';
|
|
||||||
import 'package:pweb/widgets/error/snackbar.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationListView extends StatefulWidget {
|
|
||||||
const InvitationListView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<InvitationListView> createState() => _InvitationListViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InvitationListViewState extends State<InvitationListView> {
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
|
|
||||||
InvitationFilter _filter = InvitationFilter.all;
|
|
||||||
String _query = '';
|
|
||||||
Object? _lastError;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_searchController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setQuery(String query) {
|
|
||||||
setState(() => _query = query.trim().toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setFilter(InvitationFilter filter) {
|
|
||||||
setState(() => _filter = filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _notifyError(BuildContext context, Object error, AppLocalizations loc) {
|
|
||||||
if (identical(error, _lastError)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_lastError = error;
|
|
||||||
postNotifyUserOfErrorX(
|
|
||||||
context: context,
|
|
||||||
errorSituation: loc.errorLoadingInvitations,
|
|
||||||
exception: error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Invitation> _filteredInvitations(List<Invitation> invitations) {
|
|
||||||
final showArchived = _filter == InvitationFilter.archived;
|
|
||||||
Iterable<Invitation> filtered = invitations
|
|
||||||
.where((inv) => showArchived ? inv.isArchived : !inv.isArchived)
|
|
||||||
.where((inv) => invitationFilterMatches(_filter, inv));
|
|
||||||
|
|
||||||
if (_query.isNotEmpty) {
|
|
||||||
filtered = filtered.where((inv) {
|
|
||||||
return inv.inviteeDisplayName.toLowerCase().contains(_query)
|
|
||||||
|| inv.content.email.toLowerCase().contains(_query);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final sorted = filtered.toList()
|
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final provider = context.watch<InvitationsProvider>();
|
|
||||||
|
|
||||||
if (provider.isLoading) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.error != null) {
|
|
||||||
_notifyError(context, provider.error!, loc);
|
|
||||||
} else {
|
|
||||||
_lastError = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final invitations = _filteredInvitations(provider.invitations);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
InvitationSearchField(
|
|
||||||
controller: _searchController,
|
|
||||||
hintText: loc.invitationSearchHint,
|
|
||||||
onChanged: _setQuery,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
InvitationFilterChips(
|
|
||||||
selectedFilter: _filter,
|
|
||||||
onSelected: _setFilter,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
InvitationListBody(invitations: invitations),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationSearchField extends StatelessWidget {
|
|
||||||
final TextEditingController controller;
|
|
||||||
final String hintText;
|
|
||||||
final ValueChanged<String> onChanged;
|
|
||||||
|
|
||||||
const InvitationSearchField({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
required this.hintText,
|
|
||||||
required this.onChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.search),
|
|
||||||
hintText: hintText,
|
|
||||||
),
|
|
||||||
onChanged: onChanged,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,6 @@ class _CardFormMinimalState extends State<CardFormMinimal> {
|
|||||||
late TextEditingController _panController;
|
late TextEditingController _panController;
|
||||||
late TextEditingController _firstNameController;
|
late TextEditingController _firstNameController;
|
||||||
late TextEditingController _lastNameController;
|
late TextEditingController _lastNameController;
|
||||||
late TextEditingController _expiryController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -38,23 +37,17 @@ class _CardFormMinimalState extends State<CardFormMinimal> {
|
|||||||
_panController = TextEditingController(text: widget.initialData?.pan ?? '');
|
_panController = TextEditingController(text: widget.initialData?.pan ?? '');
|
||||||
_firstNameController = TextEditingController(text: widget.initialData?.firstName ?? '');
|
_firstNameController = TextEditingController(text: widget.initialData?.firstName ?? '');
|
||||||
_lastNameController = TextEditingController(text: widget.initialData?.lastName ?? '');
|
_lastNameController = TextEditingController(text: widget.initialData?.lastName ?? '');
|
||||||
_expiryController = TextEditingController(text: _formatExpiry(widget.initialData));
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _emitIfValid() {
|
void _emitIfValid() {
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
final expiry = _parseExpiry(_expiryController.text);
|
|
||||||
if (expiry == null) return;
|
|
||||||
|
|
||||||
widget.onChanged(
|
widget.onChanged(
|
||||||
CardPaymentMethod(
|
CardPaymentMethod(
|
||||||
pan: _panController.text.replaceAll(' ', ''),
|
pan: _panController.text.replaceAll(' ', ''),
|
||||||
firstName: _firstNameController.text,
|
firstName: _firstNameController.text,
|
||||||
lastName: _lastNameController.text,
|
lastName: _lastNameController.text,
|
||||||
expMonth: expiry.month,
|
|
||||||
expYear: expiry.year,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,7 +63,6 @@ class _CardFormMinimalState extends State<CardFormMinimal> {
|
|||||||
_panController.clear();
|
_panController.clear();
|
||||||
_firstNameController.clear();
|
_firstNameController.clear();
|
||||||
_lastNameController.clear();
|
_lastNameController.clear();
|
||||||
_expiryController.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +70,12 @@ class _CardFormMinimalState extends State<CardFormMinimal> {
|
|||||||
final hasPanChange = newData.pan != _panController.text;
|
final hasPanChange = newData.pan != _panController.text;
|
||||||
final hasFirstNameChange = newData.firstName != _firstNameController.text;
|
final hasFirstNameChange = newData.firstName != _firstNameController.text;
|
||||||
final hasLastNameChange = newData.lastName != _lastNameController.text;
|
final hasLastNameChange = newData.lastName != _lastNameController.text;
|
||||||
final hasExpiryChange = _formatExpiry(newData) != _expiryController.text;
|
|
||||||
|
|
||||||
if (hasPanChange) _panController.text = newData.pan;
|
if (hasPanChange) _panController.text = newData.pan;
|
||||||
if (hasFirstNameChange) _firstNameController.text = newData.firstName;
|
if (hasFirstNameChange) _firstNameController.text = newData.firstName;
|
||||||
if (hasLastNameChange) _lastNameController.text = newData.lastName;
|
if (hasLastNameChange) _lastNameController.text = newData.lastName;
|
||||||
if (hasExpiryChange) _expiryController.text = _formatExpiry(newData);
|
|
||||||
|
|
||||||
if (hasPanChange || hasFirstNameChange || hasLastNameChange || hasExpiryChange) {
|
if (hasPanChange || hasFirstNameChange || hasLastNameChange) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,16 +115,6 @@ class _CardFormMinimalState extends State<CardFormMinimal> {
|
|||||||
style: getTextFieldStyle(context, widget.isEditable),
|
style: getTextFieldStyle(context, widget.isEditable),
|
||||||
validator: (v) => (v == null || v.isEmpty) ? l10n.enterLastName : null,
|
validator: (v) => (v == null || v.isEmpty) ? l10n.enterLastName : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextFormField(
|
|
||||||
readOnly: !widget.isEditable,
|
|
||||||
controller: _expiryController,
|
|
||||||
decoration: getInputDecoration(context, l10n.expiryDate, widget.isEditable),
|
|
||||||
style: getTextFieldStyle(context, widget.isEditable),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [CreditCardExpirationDateFormatter()],
|
|
||||||
validator: (v) => _parseExpiry(v ?? '') == null ? l10n.enterExpiryDate : null,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -145,35 +125,6 @@ class _CardFormMinimalState extends State<CardFormMinimal> {
|
|||||||
_panController.dispose();
|
_panController.dispose();
|
||||||
_firstNameController.dispose();
|
_firstNameController.dispose();
|
||||||
_lastNameController.dispose();
|
_lastNameController.dispose();
|
||||||
_expiryController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatExpiry(CardPaymentMethod? data) {
|
|
||||||
if (data == null) return '';
|
|
||||||
if (data.expMonth == null || data.expYear == null) return '';
|
|
||||||
final month = data.expMonth!.toString().padLeft(2, '0');
|
|
||||||
final year = (data.expYear! % 100).toString().padLeft(2, '0');
|
|
||||||
return '$month/$year';
|
|
||||||
}
|
|
||||||
|
|
||||||
_CardExpiry? _parseExpiry(String value) {
|
|
||||||
final normalized = value.trim();
|
|
||||||
final match = RegExp(r'^(\d{2})\s*/\s*(\d{2})$').firstMatch(normalized);
|
|
||||||
if (match == null) return null;
|
|
||||||
|
|
||||||
final month = int.tryParse(match.group(1)!);
|
|
||||||
final year = int.tryParse(match.group(2)!);
|
|
||||||
if (month == null || year == null || month < 1 || month > 12) return null;
|
|
||||||
|
|
||||||
final normalizedYear = 2000 + year;
|
|
||||||
return _CardExpiry(month: month, year: normalizedYear);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CardExpiry {
|
|
||||||
final int month;
|
|
||||||
final int year;
|
|
||||||
|
|
||||||
const _CardExpiry({required this.month, required this.year});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
import 'package:pshared/models/recipient/recipient.dart';
|
import 'package:pshared/models/recipient/recipient.dart';
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/dashboard/payouts/form.dart';
|
import 'package:pweb/pages/dashboard/payouts/form.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/payment_methods/payment_page/back_button.dart';
|
import 'package:pweb/pages/payment_methods/payment_page/back_button.dart';
|
||||||
import 'package:pweb/pages/payment_methods/payment_page/header.dart';
|
import 'package:pweb/pages/payment_methods/payment_page/header.dart';
|
||||||
import 'package:pweb/pages/payment_methods/payment_page/method_selector.dart';
|
import 'package:pweb/pages/payment_methods/payment_page/method_selector.dart';
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class PaymentMethodDropdown extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => DropdownButtonFormField<Wallet>(
|
Widget build(BuildContext context) => DropdownButtonFormField<Wallet>(
|
||||||
dropdownColor: Theme.of(context).colorScheme.onSecondary,
|
dropdownColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
initialValue: _getSelectedMethod(),
|
value: _getSelectedMethod(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context)!.whereGetMoney,
|
labelText: AppLocalizations.of(context)!.whereGetMoney,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ enum PayoutDestination {
|
|||||||
dashboard(Icons.dashboard_outlined, 'dashboard'),
|
dashboard(Icons.dashboard_outlined, 'dashboard'),
|
||||||
sendPayout(Icons.send_outlined, 'sendPayout'),
|
sendPayout(Icons.send_outlined, 'sendPayout'),
|
||||||
recipients(Icons.people_outline, 'recipients'),
|
recipients(Icons.people_outline, 'recipients'),
|
||||||
invitations(Icons.mark_email_read_outlined, 'invitations'),
|
|
||||||
reports(Icons.insert_chart, 'reports'),
|
reports(Icons.insert_chart, 'reports'),
|
||||||
settings(Icons.settings_outlined, 'settings'),
|
settings(Icons.settings_outlined, 'settings'),
|
||||||
methods(Icons.credit_card, 'methods'),
|
methods(Icons.credit_card, 'methods'),
|
||||||
@@ -30,15 +29,13 @@ enum PayoutDestination {
|
|||||||
case PayoutDestination.sendPayout:
|
case PayoutDestination.sendPayout:
|
||||||
return loc.payoutNavSendPayout;
|
return loc.payoutNavSendPayout;
|
||||||
case PayoutDestination.recipients:
|
case PayoutDestination.recipients:
|
||||||
return loc.payoutNavRecipients;
|
return loc.payoutNavRecipients;
|
||||||
case PayoutDestination.reports:
|
case PayoutDestination.reports:
|
||||||
return loc.payoutNavReports;
|
return loc.payoutNavReports;
|
||||||
case PayoutDestination.settings:
|
case PayoutDestination.settings:
|
||||||
return loc.payoutNavSettings;
|
return loc.payoutNavSettings;
|
||||||
case PayoutDestination.methods:
|
case PayoutDestination.methods:
|
||||||
return loc.payoutNavMethods;
|
return loc.payoutNavMethods;
|
||||||
case PayoutDestination.invitations:
|
|
||||||
return loc.payoutNavInvitations;
|
|
||||||
case PayoutDestination.payment:
|
case PayoutDestination.payment:
|
||||||
return loc.payout;
|
return loc.payout;
|
||||||
case PayoutDestination.addrecipient:
|
case PayoutDestination.addrecipient:
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class PageSelector extends StatelessWidget {
|
|||||||
PayoutDestination.methods,
|
PayoutDestination.methods,
|
||||||
PayoutDestination.editwallet,
|
PayoutDestination.editwallet,
|
||||||
PayoutDestination.walletTopUp,
|
PayoutDestination.walletTopUp,
|
||||||
PayoutDestination.invitations,
|
|
||||||
}
|
}
|
||||||
: PayoutDestination.values.toSet();
|
: PayoutDestination.values.toSet();
|
||||||
|
|
||||||
@@ -104,9 +103,6 @@ class PageSelector extends StatelessWidget {
|
|||||||
if (location.startsWith(PayoutRoutes.recipientsPath)) {
|
if (location.startsWith(PayoutRoutes.recipientsPath)) {
|
||||||
return PayoutDestination.recipients;
|
return PayoutDestination.recipients;
|
||||||
}
|
}
|
||||||
if (location.startsWith(PayoutRoutes.invitationsPath)) {
|
|
||||||
return PayoutDestination.invitations;
|
|
||||||
}
|
|
||||||
if (location.startsWith(PayoutRoutes.settingsPath)) {
|
if (location.startsWith(PayoutRoutes.settingsPath)) {
|
||||||
return PayoutDestination.settings;
|
return PayoutDestination.settings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class PayoutSidebar extends StatelessWidget {
|
|||||||
<PayoutDestination>[
|
<PayoutDestination>[
|
||||||
PayoutDestination.dashboard,
|
PayoutDestination.dashboard,
|
||||||
PayoutDestination.recipients,
|
PayoutDestination.recipients,
|
||||||
PayoutDestination.invitations,
|
|
||||||
PayoutDestination.methods,
|
PayoutDestination.methods,
|
||||||
PayoutDestination.reports,
|
PayoutDestination.reports,
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user