245 lines
8.5 KiB
Dart
245 lines
8.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../services/clubs/club_models.dart';
|
|
|
|
/// Semantic state representing the viewer's relationship to a Club.
|
|
enum ClubMembershipPresentationState {
|
|
unauthenticated,
|
|
outsiderOpen,
|
|
outsiderRequest,
|
|
outsiderInviteOnly,
|
|
invited,
|
|
requested,
|
|
activeMember,
|
|
activeAdmin,
|
|
activeOwner,
|
|
archived,
|
|
unavailable,
|
|
}
|
|
|
|
/// Action intent triggered by the rider on a Club.
|
|
enum ClubMembershipAction {
|
|
joinOpen,
|
|
requestToJoin,
|
|
withdrawRequest,
|
|
acceptInvite,
|
|
declineInvite,
|
|
leaveClub,
|
|
signIn,
|
|
}
|
|
|
|
/// Badge color categorization for automotive UI styling.
|
|
enum PresentationBadgeType {
|
|
cyan,
|
|
green,
|
|
amber,
|
|
neutral,
|
|
}
|
|
|
|
/// Pure presentation model mapping Club metadata, membership state, and auth context
|
|
/// into authoritative, unambiguous UX components.
|
|
@immutable
|
|
class ClubMembershipPresentation {
|
|
final ClubMembershipPresentationState state;
|
|
final String statusBadge;
|
|
final PresentationBadgeType badgeType;
|
|
final String statusHeadline;
|
|
final String statusDescription;
|
|
final ClubMembershipAction? primaryAction;
|
|
final String? primaryActionLabel;
|
|
final IconData? primaryActionIcon;
|
|
final bool isPrimaryDestructive;
|
|
final ClubMembershipAction? secondaryAction;
|
|
final String? secondaryActionLabel;
|
|
final IconData? secondaryActionIcon;
|
|
final bool isSecondaryDestructive;
|
|
final bool canLeave;
|
|
final bool isActionable;
|
|
|
|
const ClubMembershipPresentation({
|
|
required this.state,
|
|
required this.statusBadge,
|
|
required this.badgeType,
|
|
required this.statusHeadline,
|
|
required this.statusDescription,
|
|
this.primaryAction,
|
|
this.primaryActionLabel,
|
|
this.primaryActionIcon,
|
|
this.isPrimaryDestructive = false,
|
|
this.secondaryAction,
|
|
this.secondaryActionLabel,
|
|
this.secondaryActionIcon,
|
|
this.isSecondaryDestructive = false,
|
|
this.canLeave = false,
|
|
this.isActionable = false,
|
|
});
|
|
|
|
/// Resolves the pure presentation state from domain models and auth context.
|
|
static ClubMembershipPresentation resolve({
|
|
required Club? club,
|
|
required ClubMember? member,
|
|
required String? currentUserId,
|
|
required bool isAuthenticated,
|
|
}) {
|
|
if (club == null) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.unavailable,
|
|
statusBadge: 'UNAVAILABLE',
|
|
badgeType: PresentationBadgeType.neutral,
|
|
statusHeadline: 'Club Not Found',
|
|
statusDescription: 'This club could not be loaded or does not exist.',
|
|
isActionable: false,
|
|
);
|
|
}
|
|
|
|
if (club.isArchived) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.archived,
|
|
statusBadge: 'ARCHIVED',
|
|
badgeType: PresentationBadgeType.amber,
|
|
statusHeadline: 'Club Archived',
|
|
statusDescription:
|
|
'This club has been archived by leadership and is no longer accepting members.',
|
|
canLeave: false,
|
|
isActionable: false,
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated || currentUserId == null || currentUserId.isEmpty) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.unauthenticated,
|
|
statusBadge: 'GUEST',
|
|
badgeType: PresentationBadgeType.neutral,
|
|
statusHeadline: 'Sign in to Join',
|
|
statusDescription:
|
|
'Sign in with a registered account to join this club and ride with members.',
|
|
primaryAction: ClubMembershipAction.signIn,
|
|
primaryActionLabel: 'Sign In / Register',
|
|
primaryActionIcon: Icons.login,
|
|
isActionable: true,
|
|
);
|
|
}
|
|
|
|
// Authenticated user with an active membership
|
|
if (member != null && member.status == ClubMemberStatus.active) {
|
|
final isOwner = member.role == ClubRole.owner || club.ownerUid == currentUserId;
|
|
if (isOwner) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.activeOwner,
|
|
statusBadge: 'OWNER',
|
|
badgeType: PresentationBadgeType.green,
|
|
statusHeadline: 'Club Owner',
|
|
statusDescription:
|
|
'You are the owner of this club. Ownership must be transferred before leaving.',
|
|
canLeave: false,
|
|
isActionable: false,
|
|
);
|
|
}
|
|
|
|
if (member.role == ClubRole.admin) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.activeAdmin,
|
|
statusBadge: 'ADMIN',
|
|
badgeType: PresentationBadgeType.cyan,
|
|
statusHeadline: 'Club Administrator',
|
|
statusDescription: 'You are an active administrator of this club.',
|
|
primaryAction: ClubMembershipAction.leaveClub,
|
|
primaryActionLabel: 'Leave Club',
|
|
primaryActionIcon: Icons.logout,
|
|
isPrimaryDestructive: true,
|
|
canLeave: true,
|
|
isActionable: true,
|
|
);
|
|
}
|
|
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.activeMember,
|
|
statusBadge: 'MEMBER',
|
|
badgeType: PresentationBadgeType.cyan,
|
|
statusHeadline: 'Active Member',
|
|
statusDescription: 'You are an active member of this club.',
|
|
primaryAction: ClubMembershipAction.leaveClub,
|
|
primaryActionLabel: 'Leave Club',
|
|
primaryActionIcon: Icons.logout,
|
|
isPrimaryDestructive: true,
|
|
canLeave: true,
|
|
isActionable: true,
|
|
);
|
|
}
|
|
|
|
// Authenticated user with an incoming invitation
|
|
if (member != null && member.status == ClubMemberStatus.invited) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.invited,
|
|
statusBadge: 'INVITED',
|
|
badgeType: PresentationBadgeType.green,
|
|
statusHeadline: 'You Are Invited',
|
|
statusDescription: 'You have been invited to join this club by club leadership.',
|
|
primaryAction: ClubMembershipAction.acceptInvite,
|
|
primaryActionLabel: 'Accept Invitation',
|
|
primaryActionIcon: Icons.check_circle_outline,
|
|
secondaryAction: ClubMembershipAction.declineInvite,
|
|
secondaryActionLabel: 'Decline',
|
|
secondaryActionIcon: Icons.close,
|
|
isSecondaryDestructive: true,
|
|
isActionable: true,
|
|
);
|
|
}
|
|
|
|
// Authenticated user with a pending join request
|
|
if (member != null && member.status == ClubMemberStatus.requested) {
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.requested,
|
|
statusBadge: 'REQUEST PENDING',
|
|
badgeType: PresentationBadgeType.amber,
|
|
statusHeadline: 'Membership Requested',
|
|
statusDescription: 'Your request to join is awaiting approval from club leadership.',
|
|
primaryAction: ClubMembershipAction.withdrawRequest,
|
|
primaryActionLabel: 'Withdraw Request',
|
|
primaryActionIcon: Icons.cancel_outlined,
|
|
isPrimaryDestructive: true,
|
|
isActionable: true,
|
|
);
|
|
}
|
|
|
|
// Outsider (no member doc or historical doc: left, declined, removed)
|
|
switch (club.admissionPolicy) {
|
|
case ClubAdmissionPolicy.open:
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.outsiderOpen,
|
|
statusBadge: 'OPEN ADMISSION',
|
|
badgeType: PresentationBadgeType.cyan,
|
|
statusHeadline: 'Open Membership',
|
|
statusDescription: 'This club is open to all riders. Join now to participate in club rides.',
|
|
primaryAction: ClubMembershipAction.joinOpen,
|
|
primaryActionLabel: 'Join Club',
|
|
primaryActionIcon: Icons.group_add,
|
|
isActionable: true,
|
|
);
|
|
|
|
case ClubAdmissionPolicy.request:
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.outsiderRequest,
|
|
statusBadge: 'REQUEST REQUIRED',
|
|
badgeType: PresentationBadgeType.cyan,
|
|
statusHeadline: 'Request Admission',
|
|
statusDescription: 'Membership requires review and approval from club leadership.',
|
|
primaryAction: ClubMembershipAction.requestToJoin,
|
|
primaryActionLabel: 'Request to Join',
|
|
primaryActionIcon: Icons.send_outlined,
|
|
isActionable: true,
|
|
);
|
|
|
|
case ClubAdmissionPolicy.inviteOnly:
|
|
return const ClubMembershipPresentation(
|
|
state: ClubMembershipPresentationState.outsiderInviteOnly,
|
|
statusBadge: 'INVITE ONLY',
|
|
badgeType: PresentationBadgeType.neutral,
|
|
statusHeadline: 'Invite-Only Club',
|
|
statusDescription:
|
|
'This club is invite-only. You must receive an invitation from leadership to join.',
|
|
isActionable: false,
|
|
);
|
|
}
|
|
}
|
|
}
|