Trent Larson
5 days ago
7 changed files with 616 additions and 60 deletions
@ -0,0 +1,197 @@ |
|||||
|
<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"> |
||||
|
<fa icon="spinner" class="fa-spin-pulse" /> |
||||
|
</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> |
||||
|
<p class="text-sm text-gray-600">Group ID: {{ meeting.groupId }}</p> |
||||
|
</div> |
||||
|
|
||||
|
<p v-if="meetings.length === 0" 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 |
||||
|
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 |
||||
|
@click="cancelPasswordDialog" |
||||
|
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300" |
||||
|
> |
||||
|
Cancel |
||||
|
</button> |
||||
|
<button |
||||
|
@click="submitPassword" |
||||
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" |
||||
|
> |
||||
|
Submit |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator'; |
||||
|
import QuickNav from '@/components/QuickNav.vue'; |
||||
|
import TopMessage from '@/components/TopMessage.vue'; |
||||
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; |
||||
|
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; |
||||
|
import { encryptMessage } from '@/libs/crypto'; |
||||
|
|
||||
|
interface Meeting { |
||||
|
name: string; |
||||
|
groupId: number; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
QuickNav, |
||||
|
TopMessage, |
||||
|
}, |
||||
|
}) |
||||
|
export default class OnboardMeetingListView extends Vue { |
||||
|
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; |
||||
|
|
||||
|
meetings: Meeting[] = []; |
||||
|
isLoading = false; |
||||
|
showPasswordDialog = false; |
||||
|
password = ''; |
||||
|
selectedMeeting: Meeting | null = null; |
||||
|
activeDid = ''; |
||||
|
apiServer = ''; |
||||
|
firstName = ''; |
||||
|
|
||||
|
async created() { |
||||
|
const settings = await retrieveSettingsForActiveAccount(); |
||||
|
this.activeDid = settings.activeDid || ''; |
||||
|
this.apiServer = settings.apiServer || ''; |
||||
|
this.firstName = settings.firstName || ''; |
||||
|
await this.fetchMeetings(); |
||||
|
} |
||||
|
|
||||
|
async fetchMeetings() { |
||||
|
this.isLoading = true; |
||||
|
try { |
||||
|
const headers = await getHeaders(this.activeDid); |
||||
|
const response = await this.axios.get( |
||||
|
this.apiServer + '/api/partner/groupsOnboarding', |
||||
|
{ headers } |
||||
|
); |
||||
|
|
||||
|
if (response.data && response.data.data) { |
||||
|
this.meetings = response.data.data; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logConsoleAndDb('Error fetching meetings: ' + errorStringForLog(error), true); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: 'alert', |
||||
|
type: 'danger', |
||||
|
title: 'Error', |
||||
|
text: serverMessageForUser(error) || 'Failed to fetch meetings.', |
||||
|
}, |
||||
|
5000 |
||||
|
); |
||||
|
} finally { |
||||
|
this.isLoading = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
promptPassword(meeting: Meeting) { |
||||
|
this.password = ''; |
||||
|
this.selectedMeeting = meeting; |
||||
|
this.showPasswordDialog = true; |
||||
|
} |
||||
|
|
||||
|
cancelPasswordDialog() { |
||||
|
this.password = ''; |
||||
|
this.selectedMeeting = null; |
||||
|
this.showPasswordDialog = false; |
||||
|
} |
||||
|
|
||||
|
async submitPassword() { |
||||
|
if (!this.selectedMeeting) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// Create member data object |
||||
|
const memberData = { |
||||
|
name: this.firstName, |
||||
|
did: this.activeDid |
||||
|
}; |
||||
|
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) { |
||||
|
logConsoleAndDb('Error joining meeting: ' + errorStringForLog(error), true); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: 'alert', |
||||
|
type: 'danger', |
||||
|
title: 'Error', |
||||
|
text: serverMessageForUser(error) || 'Failed to join meeting.', |
||||
|
}, |
||||
|
5000 |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,157 @@ |
|||||
|
<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"> |
||||
|
Meeting Members |
||||
|
</h1> |
||||
|
|
||||
|
<!-- Back Button --> |
||||
|
<button |
||||
|
@click="$router.back()" |
||||
|
class="mb-6 flex items-center text-blue-600 hover:text-blue-800" |
||||
|
> |
||||
|
<fa icon="arrow-left" class="mr-2" /> |
||||
|
Back to Meetings |
||||
|
</button> |
||||
|
|
||||
|
<!-- Loading State --> |
||||
|
<div v-if="isLoading" class="flex justify-center items-center py-8"> |
||||
|
<fa icon="spinner" class="fa-spin-pulse" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Error State --> |
||||
|
<div v-else-if="errorMessage"> |
||||
|
<div class="text-center text-red-600 py-8"> |
||||
|
{{ errorMessage }} |
||||
|
</div> |
||||
|
<div class="text-center"> |
||||
|
For authorization, wait for your meeting organizer to approve you. |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Members List --> |
||||
|
<div v-else class="space-y-4"> |
||||
|
<div v-for="member in decryptedMembers" :key="member.memberId" |
||||
|
class="p-4 bg-white rounded-lg shadow"> |
||||
|
<h2 class="text-xl font-medium">{{ member.name }}</h2> |
||||
|
<p class="text-sm text-gray-600">DID: {{ member.did }}</p> |
||||
|
</div> |
||||
|
|
||||
|
<p v-if="decryptedMembers.length === 0 && !decryptFailure" class="text-center text-gray-500 py-8"> |
||||
|
No members found in this meeting |
||||
|
</p> |
||||
|
<p v-if="decryptFailure" class="text-center text-red-600 py-8"> |
||||
|
That password failed. You may be in the wrong meeting. Go back and try again. |
||||
|
</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator'; |
||||
|
import QuickNav from '@/components/QuickNav.vue'; |
||||
|
import TopMessage from '@/components/TopMessage.vue'; |
||||
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; |
||||
|
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; |
||||
|
import { decryptMessage } from '@/libs/crypto'; |
||||
|
|
||||
|
interface Member { |
||||
|
memberId: number; |
||||
|
content: string; |
||||
|
} |
||||
|
|
||||
|
interface DecryptedMember { |
||||
|
memberId: number; |
||||
|
name: string; |
||||
|
did: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
QuickNav, |
||||
|
TopMessage, |
||||
|
}, |
||||
|
}) |
||||
|
export default class OnboardMeetingMembersView extends Vue { |
||||
|
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; |
||||
|
|
||||
|
activeDid = ''; |
||||
|
apiServer = ''; |
||||
|
decryptedMembers: DecryptedMember[] = []; |
||||
|
decryptFailure = false; |
||||
|
errorMessage = ''; |
||||
|
isLoading = false; |
||||
|
members: Member[] = []; |
||||
|
|
||||
|
get groupId(): string { |
||||
|
return this.$route.params.groupId as string; |
||||
|
} |
||||
|
|
||||
|
get password(): string { |
||||
|
return this.$route.query.password as string; |
||||
|
} |
||||
|
|
||||
|
async created() { |
||||
|
if (!this.groupId) { |
||||
|
this.errorMessage = 'The group info is missing. Go back and try again.'; |
||||
|
return; |
||||
|
} |
||||
|
if (!this.password) { |
||||
|
this.errorMessage = 'The password is missing. Go back and try again.'; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const settings = await retrieveSettingsForActiveAccount(); |
||||
|
this.activeDid = settings.activeDid || ''; |
||||
|
this.apiServer = settings.apiServer || ''; |
||||
|
await this.fetchMembers(); |
||||
|
} |
||||
|
|
||||
|
async fetchMembers() { |
||||
|
this.isLoading = true; |
||||
|
this.errorMessage = ''; |
||||
|
|
||||
|
try { |
||||
|
const headers = await getHeaders(this.activeDid); |
||||
|
const response = await this.axios.get( |
||||
|
`${this.apiServer}/api/partner/groupOnboardMembers/`, |
||||
|
{ headers } |
||||
|
); |
||||
|
|
||||
|
if (response.data && response.data.data) { |
||||
|
this.members = response.data.data; |
||||
|
await this.decryptMemberContents(); |
||||
|
} else { |
||||
|
throw { response: response }; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logConsoleAndDb('Error fetching members: ' + errorStringForLog(error), true); |
||||
|
this.errorMessage = serverMessageForUser(error) || 'Failed to fetch members.'; |
||||
|
} finally { |
||||
|
this.isLoading = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async decryptMemberContents() { |
||||
|
this.decryptedMembers = []; |
||||
|
|
||||
|
for (const member of this.members) { |
||||
|
try { |
||||
|
const decryptedContent = await decryptMessage(member.content, this.password); |
||||
|
const content = JSON.parse(decryptedContent); |
||||
|
|
||||
|
this.decryptedMembers.push({ |
||||
|
memberId: member.memberId, |
||||
|
name: content.name, |
||||
|
did: content.did, |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
this.decryptFailure = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
Loading…
Reference in new issue