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