You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

507 lines
16 KiB

<template>
<QuickNav selected="Contacts" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
Onboarding Meetings
</h1>
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else-if="attendingMeeting">
<p>You are in this meeting.</p>
<div
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
@click="promptPassword(attendingMeeting)"
>
<div class="flex justify-between items-center">
<h2 class="text-xl font-medium">{{ attendingMeeting.name }}</h2>
<button
class="text-red-600 hover:text-red-700 p-2"
title="Leave Meeting"
@click.stop="leaveMeeting"
>
<font-awesome icon="right-from-bracket" />
</button>
</div>
</div>
</div>
<!-- Meeting List -->
<div v-else class="space-y-4">
<div
v-for="meeting in meetings"
:key="meeting.groupId"
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
@click="promptPassword(meeting)"
>
<h2 class="text-xl font-medium">{{ meeting.name }}</h2>
</div>
<!-- Create Meeting Button for registered users with no meetings -->
<div
v-if="meetings.length === 0 && isRegistered"
class="text-center py-8"
>
<p class="text-gray-500 mb-4">No onboarding meetings available</p>
<button
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-6 py-3 rounded-md hover:from-green-500 hover:to-green-800 transition-colors"
@click="createMeeting"
>
<font-awesome icon="plus" class="mr-2" />
Create Meeting
</button>
</div>
<!-- No meetings message for unregistered users -->
<p
v-if="meetings.length === 0 && !isRegistered"
class="text-center text-gray-500 py-8"
>
No onboarding meetings available
</p>
</div>
<!-- Password Dialog -->
<div
v-if="showPasswordDialog"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"
>
<div class="bg-white rounded-lg p-6 max-w-sm w-full">
<h3 class="text-lg font-medium mb-4">Enter Meeting Password</h3>
<input
ref="passwordInput"
v-model="password"
type="text"
class="w-full px-3 py-2 border rounded-md mb-4"
placeholder="Enter password"
@keyup.enter="submitPassword"
/>
<div class="flex justify-end space-x-4">
<button
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
@click="cancelPasswordDialog"
>
Cancel
</button>
<button
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
@click="submitPassword"
>
Submit
</button>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { encryptMessage } from "../libs/crypto";
import {
errorStringForLog,
getHeaders,
serverMessageForUser,
} from "../libs/endorserServer";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NotificationIface } from "@/constants/app";
interface Meeting {
name: string;
groupId: number;
}
@Component({
components: {
QuickNav,
TopMessage,
},
mixins: [PlatformServiceMixin],
})
export default class OnboardMeetingListView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
apiServer = "";
attendingMeeting: Meeting | null = null;
firstName = "";
isLoading = false;
isRegistered = false;
meetings: Meeting[] = [];
password = "";
selectedMeeting: Meeting | null = null;
showPasswordDialog = false;
/**
* Vue lifecycle hook - component initialization
*
* Initializes the component by loading user settings and fetching available
* onboarding meetings. This method is called when the component is created
* and sets up all necessary data for the meeting list interface.
*
* Workflow:
* 1. Initialize notification system using createNotifyHelpers
* 2. Load user account settings (DID, API server, registration status)
* 3. Fetch available onboarding meetings from the server
*
* Dependencies:
* - PlatformServiceMixin for settings access ($accountSettings)
* - Server API for meeting data (fetchMeetings)
*
* Error Handling:
* - Server errors during meeting fetch are handled in fetchMeetings()
*
* @author Matthew Raymer
*/
async created() {
this.notify = createNotifyHelpers(this.$notify);
// Load user account settings
const settings = await this.$accountSettings();
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;
await this.fetchMeetings();
}
/**
* Fetches available onboarding meetings from the server
*
* This method retrieves the list of onboarding meetings that the user can join.
* It first checks if the user is already attending a meeting, and if so,
* displays that meeting instead of the full list.
*
* Workflow:
* 1. Check if user is already attending a meeting (groupOnboardMember endpoint)
* 2. If attending: Fetch meeting details and display single meeting view
* 3. If not attending: Fetch all available meetings (groupsOnboarding endpoint)
* 4. Handle loading states and error conditions
*
* API Endpoints Used:
* - GET /api/partner/groupOnboardMember - Check current attendance
* - GET /api/partner/groupOnboard/{id} - Get meeting details
* - GET /api/partner/groupsOnboarding - Get all available meetings
*
* State Management:
* - Sets isLoading flag during API calls
* - Updates attendingMeeting or meetings array
* - Handles error states with user notifications
*
* @author Matthew Raymer
*/
async fetchMeetings() {
this.isLoading = true;
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
this.apiServer + "/api/partner/groupOnboardMember",
{ headers },
);
if (response.data?.data) {
const attendingMeetingId = response.data.data.groupId;
const headers2 = await getHeaders(this.activeDid);
const response2 = await this.axios.get(
this.apiServer + "/api/partner/groupOnboard/" + attendingMeetingId,
{ headers: headers2 },
);
if (response2.data?.data) {
this.attendingMeeting = response2.data.data;
return;
} else {
this.$logAndConsole(
"Error fetching meeting for user after saying they are in one.",
true,
);
}
} else {
this.$logAndConsole(
"Error fetching meeting for user after saying they are in one.",
true,
);
}
const headers2 = await getHeaders(this.activeDid);
const response2 = await this.axios.get(
this.apiServer + "/api/partner/groupsOnboarding",
{ headers: headers2 },
);
if (response2.data?.data) {
this.meetings = response2.data.data;
}
} catch (error: unknown) {
this.$logAndConsole(
"Error fetching meetings: " + errorStringForLog(error),
true,
);
this.notify.error(
serverMessageForUser(error) || "There was a problem fetching meetings.",
TIMEOUTS.LONG,
);
} finally {
this.isLoading = false;
}
}
/**
* Opens the password dialog for joining a meeting
*
* This method initiates the process of joining an onboarding meeting by
* opening a modal dialog that prompts the user for the meeting password.
* The dialog is focused and ready for input when displayed.
*
* Workflow:
* 1. Clear any previous password input
* 2. Store the selected meeting for later use
* 3. Show the password dialog modal
* 4. Focus the password input field for immediate typing
*
* UI State Changes:
* - Sets showPasswordDialog to true (shows modal)
* - Clears password field for fresh input
* - Stores selectedMeeting for password submission
*
* @param meeting - The meeting object the user wants to join
* @author Matthew Raymer
*/
promptPassword(meeting: Meeting) {
this.password = "";
this.selectedMeeting = meeting;
this.showPasswordDialog = true;
nextTick(() => {
const input = this.$refs.passwordInput as HTMLInputElement;
if (input) {
input.focus();
}
});
}
/**
* Cancels the password dialog and resets state
*
* This method handles the cancellation of the meeting password dialog.
* It cleans up the dialog state and resets all related variables to
* their initial state, ensuring a clean slate for future dialog interactions.
*
* State Cleanup:
* - Clears password input field
* - Removes selected meeting reference
* - Hides password dialog modal
*
* This ensures that if the user reopens the dialog, they start with
* a fresh state without any leftover data from previous attempts.
*
* @author Matthew Raymer
*/
cancelPasswordDialog() {
this.password = "";
this.selectedMeeting = null;
this.showPasswordDialog = false;
}
/**
* Submits the password and joins the selected meeting
*
* This method handles the complete workflow of joining an onboarding meeting.
* It encrypts the user's member data with the provided password and sends
* it to the server to register the user as a meeting participant.
*
* Workflow:
* 1. Validate that a meeting is selected (safety check)
* 2. Create member data object with user information
* 3. Encrypt member data using the meeting password
* 4. Send encrypted data to server via groupOnboardMember endpoint
* 5. On success: Navigate to meeting members view with credentials
* 6. On failure: Show error notification to user
*
* Data Encryption:
* - Member data includes: name, DID, registration status
* - Data is encrypted using the meeting password for security
* - Encrypted data is sent to server for verification
*
* Navigation:
* - On successful join: Redirects to onboard-meeting-members view
* - Passes groupId, password, and memberId as route parameters
* - Allows user to see other meeting participants
*
* Error Handling:
* - Invalid passwords result in server rejection
* - Network errors are caught and displayed to user
* - All errors are logged for debugging purposes
*
* @author Matthew Raymer
*/
async submitPassword() {
if (!this.selectedMeeting) {
// this should never happen
this.$logAndConsole(
"No meeting selected when prompting for password, which should never happen.",
true,
);
return;
}
try {
// Create member data object
const memberData = {
name: this.firstName,
did: this.activeDid,
isRegistered: this.isRegistered,
};
const memberDataString = JSON.stringify(memberData);
const encryptedMemberData = await encryptMessage(
memberDataString,
this.password,
);
// Get headers for authentication
const headers = await getHeaders(this.activeDid);
// Encrypt the member data
const postResult = await this.axios.post(
this.apiServer + "/api/partner/groupOnboardMember",
{
groupId: this.selectedMeeting.groupId,
content: encryptedMemberData,
},
{ headers },
);
if (postResult.data && postResult.data.success) {
// Navigate to members view with password and groupId
this.$router.push({
name: "onboard-meeting-members",
params: {
groupId: this.selectedMeeting.groupId.toString(),
},
query: {
password: this.password,
memberId: postResult.data.memberId,
},
});
this.cancelPasswordDialog();
} else {
throw { response: postResult };
}
} catch (error) {
this.$logAndConsole(
"Error joining meeting: " + errorStringForLog(error),
true,
);
this.notify.error(
serverMessageForUser(error) ||
"There was a problem joining the meeting.",
TIMEOUTS.LONG,
);
}
}
/**
* Prompts user to confirm leaving the current meeting
*
* This method initiates the process of leaving an onboarding meeting.
* It shows a confirmation dialog to prevent accidental departures,
* then handles the server-side removal and UI updates.
*
* Workflow:
* 1. Display confirmation dialog asking user to confirm departure
* 2. On confirmation: Send DELETE request to groupOnboardMember endpoint
* 3. On success: Clear attending meeting state and refresh meeting list
* 4. Show success notification to user
* 5. On failure: Show error notification with details
*
* Server Interaction:
* - DELETE /api/partner/groupOnboardMember - Removes user from meeting
* - Requires authentication headers for user verification
* - Server handles the actual removal from meeting database
*
* State Management:
* - Clears attendingMeeting when successfully left
* - Refreshes meetings list to show updated availability
* - Updates UI to show meeting list instead of single meeting
*
* User Experience:
* - Confirmation prevents accidental departures
* - Clear feedback on success/failure
* - Seamless transition back to meeting list
*
* @author Matthew Raymer
*/
async leaveMeeting() {
this.notify.confirm(
"Are you sure you want to leave this meeting?",
async () => {
try {
const headers = await getHeaders(this.activeDid);
await this.axios.delete(
this.apiServer + "/api/partner/groupOnboardMember",
{ headers },
);
this.attendingMeeting = null;
await this.fetchMeetings();
this.notify.success("You left the meeting.", TIMEOUTS.LONG);
} catch (error) {
this.$logAndConsole(
"Error leaving meeting: " + errorStringForLog(error),
true,
);
this.notify.error(
serverMessageForUser(error) ||
"There was a problem leaving the meeting.",
TIMEOUTS.LONG,
);
}
},
);
}
/**
* Navigates to the meeting creation page
*
* This method handles the navigation to the meeting setup page where
* registered users can create new onboarding meetings. It's only
* available to users who are registered in the system.
*
* Navigation:
* - Routes to onboard-meeting-setup view
* - Allows user to configure new meeting settings
* - Only accessible to registered users (controlled by template)
*
* User Flow:
* - User clicks "Create Meeting" button
* - System navigates to setup page
* - User can configure meeting name, password, etc.
* - New meeting becomes available to other users
*
* @author Matthew Raymer
*/
createMeeting() {
this.$router.push({ name: "onboard-meeting-setup" });
}
}
</script>