Sender Invitation

This commit is contained in:
Arseni
2026-01-12 21:28:18 +03:00
parent dedde76dd7
commit 5447433b5d
34 changed files with 1575 additions and 27 deletions

View File

@@ -0,0 +1,22 @@
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);
}

View File

@@ -0,0 +1,56 @@
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);
}

View File

@@ -0,0 +1,78 @@
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';
}
}

View File

@@ -0,0 +1,111 @@
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,
);

View File

@@ -0,0 +1,7 @@
enum InvitationStatus {
created,
sent,
accepted,
declined,
revoked,
}

View File

@@ -0,0 +1,64 @@
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,
);
}
}

View File

@@ -0,0 +1,45 @@
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,
);
}
}