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
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>
|
|
|