Sender Invitation
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
15
frontend/pweb/lib/pages/invitations/widgets/card/card.dart
Normal file
15
frontend/pweb/lib/pages/invitations/widgets/card/card.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
49
frontend/pweb/lib/pages/invitations/widgets/card/header.dart
Normal file
49
frontend/pweb/lib/pages/invitations/widgets/card/header.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
}
|
||||
72
frontend/pweb/lib/pages/invitations/widgets/card/view.dart
Normal file
72
frontend/pweb/lib/pages/invitations/widgets/card/view.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
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),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user