12 changed files with 851 additions and 71 deletions
			
			
		@ -0,0 +1,190 @@ | 
				
			|||
<template> | 
				
			|||
  <section id="Content"> | 
				
			|||
    <div v-if="claimData"> | 
				
			|||
      <canvas ref="claimCanvas"></canvas> | 
				
			|||
    </div> | 
				
			|||
  </section> | 
				
			|||
</template> | 
				
			|||
 | 
				
			|||
<style scoped> | 
				
			|||
canvas { | 
				
			|||
  position: absolute; | 
				
			|||
  top: 0; | 
				
			|||
  left: 0; | 
				
			|||
  width: 100%; | 
				
			|||
  height: 100%; | 
				
			|||
} | 
				
			|||
</style> | 
				
			|||
 | 
				
			|||
<script lang="ts"> | 
				
			|||
import { Component, Vue } from "vue-facing-decorator"; | 
				
			|||
import { nextTick } from "vue"; | 
				
			|||
import QRCode from "qrcode"; | 
				
			|||
 | 
				
			|||
import { APP_SERVER, NotificationIface } from "@/constants/app"; | 
				
			|||
import { db, retrieveSettingsForActiveAccount } from "@/db/index"; | 
				
			|||
import * as endorserServer from "@/libs/endorserServer"; | 
				
			|||
 | 
				
			|||
@Component | 
				
			|||
export default class ClaimReportCertificateView extends Vue { | 
				
			|||
  $notify!: (notification: NotificationIface, timeout?: number) => void; | 
				
			|||
 | 
				
			|||
  activeDid = ""; | 
				
			|||
  allMyDids: Array<string> = []; | 
				
			|||
  apiServer = ""; | 
				
			|||
  claimId = ""; | 
				
			|||
  claimData = null; | 
				
			|||
 | 
				
			|||
  endorserServer = endorserServer; | 
				
			|||
 | 
				
			|||
  async created() { | 
				
			|||
    const settings = await retrieveSettingsForActiveAccount(); | 
				
			|||
    this.activeDid = settings.activeDid || ""; | 
				
			|||
    this.apiServer = settings.apiServer || ""; | 
				
			|||
    const pathParams = window.location.pathname.substring( | 
				
			|||
      "/claim-cert/".length, | 
				
			|||
    ); | 
				
			|||
    this.claimId = pathParams; | 
				
			|||
    await this.fetchClaim(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async fetchClaim() { | 
				
			|||
    try { | 
				
			|||
      const response = await fetch( | 
				
			|||
        `${this.apiServer}/api/claim/${this.claimId}`, | 
				
			|||
      ); | 
				
			|||
      if (response.ok) { | 
				
			|||
        this.claimData = await response.json(); | 
				
			|||
        await nextTick(); // Wait for the DOM to update | 
				
			|||
        if (this.claimData) { | 
				
			|||
          this.drawCanvas(this.claimData); | 
				
			|||
        } | 
				
			|||
      } else { | 
				
			|||
        throw new Error(`Error fetching claim: ${response.statusText}`); | 
				
			|||
      } | 
				
			|||
    } catch (error) { | 
				
			|||
      console.error("Failed to load claim:", error); | 
				
			|||
      this.$notify({ | 
				
			|||
        group: "alert", | 
				
			|||
        type: "danger", | 
				
			|||
        title: "Error", | 
				
			|||
        text: "There was a problem loading the claim.", | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async drawCanvas( | 
				
			|||
    claimData: endorserServer.GenericCredWrapper<endorserServer.GenericVerifiableCredential>, | 
				
			|||
  ) { | 
				
			|||
    await db.open(); | 
				
			|||
    const allContacts = await db.contacts.toArray(); | 
				
			|||
 | 
				
			|||
    const canvas = this.$refs.claimCanvas as HTMLCanvasElement; | 
				
			|||
    if (canvas) { | 
				
			|||
      const CANVAS_WIDTH = 1100; | 
				
			|||
      const CANVAS_HEIGHT = 850; | 
				
			|||
 | 
				
			|||
      // size to approximate portrait of 8.5"x11" | 
				
			|||
      canvas.width = CANVAS_WIDTH; | 
				
			|||
      canvas.height = CANVAS_HEIGHT; | 
				
			|||
      const ctx = canvas.getContext("2d"); | 
				
			|||
      if (ctx) { | 
				
			|||
        // Load the background image | 
				
			|||
        const backgroundImage = new Image(); | 
				
			|||
        backgroundImage.src = "/img/background/cert-frame-2.jpg"; | 
				
			|||
        backgroundImage.onload = async () => { | 
				
			|||
          // Draw the background image | 
				
			|||
          ctx.drawImage(backgroundImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); | 
				
			|||
 | 
				
			|||
          // Set font and styles | 
				
			|||
          ctx.fillStyle = "black"; | 
				
			|||
 | 
				
			|||
          // Draw claim type | 
				
			|||
          ctx.font = "bold 20px Arial"; | 
				
			|||
          const claimTypeText = | 
				
			|||
            this.endorserServer.capitalizeAndInsertSpacesBeforeCaps( | 
				
			|||
              claimData.claimType || "", | 
				
			|||
            ); | 
				
			|||
          const claimTypeWidth = ctx.measureText(claimTypeText).width; | 
				
			|||
          ctx.fillText( | 
				
			|||
            claimTypeText, | 
				
			|||
            (CANVAS_WIDTH - claimTypeWidth) / 2, // Center horizontally | 
				
			|||
            CANVAS_HEIGHT * 0.33, | 
				
			|||
          ); | 
				
			|||
 | 
				
			|||
          if (claimData.claim.agent) { | 
				
			|||
            const presentedText = "Presented to "; | 
				
			|||
            ctx.font = "14px Arial"; | 
				
			|||
            const presentedWidth = ctx.measureText(presentedText).width; | 
				
			|||
            ctx.fillText( | 
				
			|||
              presentedText, | 
				
			|||
              (CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally | 
				
			|||
              CANVAS_HEIGHT * 0.37, | 
				
			|||
            ); | 
				
			|||
            const agentText = endorserServer.didInfoForCertificate( | 
				
			|||
              claimData.claim.agent, | 
				
			|||
              allContacts, | 
				
			|||
            ); | 
				
			|||
            ctx.font = "bold 20px Arial"; | 
				
			|||
            const agentWidth = ctx.measureText(agentText).width; | 
				
			|||
            ctx.fillText( | 
				
			|||
              agentText, | 
				
			|||
              (CANVAS_WIDTH - agentWidth) / 2, // Center horizontally | 
				
			|||
              CANVAS_HEIGHT * 0.4, | 
				
			|||
            ); | 
				
			|||
          } | 
				
			|||
 | 
				
			|||
          const descriptionText = | 
				
			|||
            claimData.claim.name || claimData.claim.description; | 
				
			|||
          if (descriptionText) { | 
				
			|||
            const descriptionLine = | 
				
			|||
              descriptionText.length > 50 | 
				
			|||
                ? descriptionText.substring(0, 75) + "..." | 
				
			|||
                : descriptionText; | 
				
			|||
            ctx.font = "14px Arial"; | 
				
			|||
            const descriptionWidth = ctx.measureText(descriptionLine).width; | 
				
			|||
            ctx.fillText( | 
				
			|||
              descriptionLine, | 
				
			|||
              (CANVAS_WIDTH - descriptionWidth) / 2, | 
				
			|||
              CANVAS_HEIGHT * 0.45, | 
				
			|||
            ); | 
				
			|||
          } | 
				
			|||
 | 
				
			|||
          // Draw claim issuer & recipient | 
				
			|||
          if (claimData.issuer) { | 
				
			|||
            ctx.font = "14px Arial"; | 
				
			|||
            const issuerText = | 
				
			|||
              "Issued by " + | 
				
			|||
              endorserServer.didInfoForCertificate( | 
				
			|||
                claimData.issuer, | 
				
			|||
                allContacts, | 
				
			|||
              ); | 
				
			|||
            ctx.fillText(issuerText, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.6); | 
				
			|||
          } | 
				
			|||
 | 
				
			|||
          // Draw claim ID | 
				
			|||
          ctx.font = "14px Arial"; | 
				
			|||
          ctx.fillText(this.claimId, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.7); | 
				
			|||
          ctx.fillText( | 
				
			|||
            "via EndorserSearch.com", | 
				
			|||
            CANVAS_WIDTH * 0.3, | 
				
			|||
            CANVAS_HEIGHT * 0.73, | 
				
			|||
          ); | 
				
			|||
 | 
				
			|||
          // Generate and draw QR code | 
				
			|||
          const qrCodeCanvas = document.createElement("canvas"); | 
				
			|||
          await QRCode.toCanvas( | 
				
			|||
            qrCodeCanvas, | 
				
			|||
            APP_SERVER + "/claim/" + this.claimId, | 
				
			|||
            { | 
				
			|||
              width: 150, | 
				
			|||
              color: { light: "#0000" /* Transparent background */ }, | 
				
			|||
            }, | 
				
			|||
          ); | 
				
			|||
          ctx.drawImage(qrCodeCanvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55); | 
				
			|||
        }; | 
				
			|||
      } | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
</script> | 
				
			|||
@ -0,0 +1,355 @@ | 
				
			|||
<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"> | 
				
			|||
      Onboarding Meeting | 
				
			|||
    </h1> | 
				
			|||
 | 
				
			|||
    <!-- Existing Meeting Section --> | 
				
			|||
    <div v-if="existingMeeting" class="mt-8 p-4 border rounded-lg bg-white shadow"> | 
				
			|||
      <h2 class="text-2xl mb-4">Current Meeting</h2> | 
				
			|||
      <div class="space-y-2"> | 
				
			|||
        <p><strong>Name:</strong> {{ existingMeeting.name }}</p> | 
				
			|||
        <p><strong>Expires:</strong> {{ formatExpirationTime(existingMeeting.expiresAt) }}</p> | 
				
			|||
        <p class="mt-4 text-sm text-gray-600">Share the the password with the people you want to onboard.</p> | 
				
			|||
      </div> | 
				
			|||
      <div class="flex justify-end mt-4"> | 
				
			|||
        <button | 
				
			|||
          @click="confirmDelete" | 
				
			|||
          class="flex items-center justify-center w-10 h-10 rounded-full bg-red-100 hover:bg-red-200 transition-colors duration-200" | 
				
			|||
          :disabled="isDeleting" | 
				
			|||
          :class="{ 'opacity-50 cursor-not-allowed': isDeleting }" | 
				
			|||
          title="Delete Meeting" | 
				
			|||
        > | 
				
			|||
          <fa icon="trash-can" class="fa-fw text-red-600" /> | 
				
			|||
          <span class="sr-only">{{ isDeleting ? 'Deleting...' : 'Delete Meeting' }}</span> | 
				
			|||
        </button> | 
				
			|||
      </div> | 
				
			|||
    </div> | 
				
			|||
 | 
				
			|||
    <!-- Delete Confirmation Dialog --> | 
				
			|||
    <div v-if="showDeleteConfirm" 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">Delete Meeting?</h3> | 
				
			|||
        <p class="text-gray-600 mb-6">This action cannot be undone. Are you sure you want to delete this meeting?</p> | 
				
			|||
        <div class="flex justify-between space-x-4"> | 
				
			|||
          <button | 
				
			|||
            @click="showDeleteConfirm = false" | 
				
			|||
            class="px-4 py-2 bg-slate-500 text-white rounded hover:bg-slate-700" | 
				
			|||
          > | 
				
			|||
            Cancel | 
				
			|||
          </button> | 
				
			|||
          <button | 
				
			|||
            @click="deleteMeeting" | 
				
			|||
            class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" | 
				
			|||
          > | 
				
			|||
            Delete | 
				
			|||
          </button> | 
				
			|||
        </div> | 
				
			|||
      </div> | 
				
			|||
    </div> | 
				
			|||
 | 
				
			|||
    <!-- Create New Meeting Form --> | 
				
			|||
    <div v-if="!existingMeeting && !isLoading" class="mt-8"> | 
				
			|||
      <h2 class="text-2xl mb-4">Create New Meeting</h2> | 
				
			|||
      <form @submit.prevent="createMeeting" class="space-y-4"> | 
				
			|||
        <div> | 
				
			|||
          <label for="meetingName" class="block text-sm font-medium text-gray-700">Meeting Name</label> | 
				
			|||
          <input | 
				
			|||
            id="meetingName" | 
				
			|||
            v-model="currentMeeting.name" | 
				
			|||
            type="text" | 
				
			|||
            required | 
				
			|||
            class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" | 
				
			|||
            placeholder="Enter meeting name" | 
				
			|||
          /> | 
				
			|||
        </div> | 
				
			|||
 | 
				
			|||
        <div> | 
				
			|||
          <label for="expirationTime" class="block text-sm font-medium text-gray-700">Meeting Expiration Time</label> | 
				
			|||
          <input | 
				
			|||
            id="expirationTime" | 
				
			|||
            v-model="currentMeeting.expiresAt" | 
				
			|||
            type="datetime-local" | 
				
			|||
            required | 
				
			|||
            :min="minDateTime" | 
				
			|||
            class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" | 
				
			|||
          /> | 
				
			|||
        </div> | 
				
			|||
 | 
				
			|||
        <div> | 
				
			|||
          <label for="password" class="block text-sm font-medium text-gray-700">Meeting Password</label> | 
				
			|||
          <input | 
				
			|||
            id="password" | 
				
			|||
            v-model="password" | 
				
			|||
            type="text" | 
				
			|||
            required | 
				
			|||
            class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" | 
				
			|||
            placeholder="Enter meeting password" | 
				
			|||
          /> | 
				
			|||
        </div> | 
				
			|||
 | 
				
			|||
        <div> | 
				
			|||
          <label for="userName" class="block text-sm font-medium text-gray-700">Your Name</label> | 
				
			|||
          <input | 
				
			|||
            id="userName" | 
				
			|||
            v-model="currentMeeting.userFullName" | 
				
			|||
            type="text" | 
				
			|||
            required | 
				
			|||
            class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" | 
				
			|||
            placeholder="Your name" | 
				
			|||
          /> | 
				
			|||
        </div> | 
				
			|||
 | 
				
			|||
        <button | 
				
			|||
          type="submit" | 
				
			|||
          class="w-full 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-4 py-2 rounded-md hover:from-green-500 hover:to-green-800" | 
				
			|||
          :disabled="isLoading" | 
				
			|||
        > | 
				
			|||
          {{ isLoading ? 'Creating...' : 'Create Meeting' }} | 
				
			|||
        </button> | 
				
			|||
      </form> | 
				
			|||
    </div> | 
				
			|||
    <div v-else-if="isLoading"> | 
				
			|||
      <div class="flex justify-center items-center h-full"> | 
				
			|||
        <fa icon="spinner" class="fa-spin-pulse" /> | 
				
			|||
      </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 { retrieveSettingsForActiveAccount } from '@/db/index'; | 
				
			|||
import { getHeaders, serverMessageForUser } from '@/libs/endorserServer'; | 
				
			|||
import { encryptMessage } from '@/libs/crypto'; | 
				
			|||
 | 
				
			|||
interface Meeting { | 
				
			|||
  groupId?: string; | 
				
			|||
  name: string; | 
				
			|||
  expiresAt: string; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
interface NewMeeting extends Meeting { | 
				
			|||
  userFullName: string; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
@Component({ | 
				
			|||
  components: { | 
				
			|||
    QuickNav, | 
				
			|||
    TopMessage, | 
				
			|||
  }, | 
				
			|||
}) | 
				
			|||
export default class OnboardMeetingView extends Vue { | 
				
			|||
  $notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; | 
				
			|||
 | 
				
			|||
  existingMeeting: Meeting | null = null; | 
				
			|||
  currentMeeting: NewMeeting = { | 
				
			|||
    name: '', | 
				
			|||
    expiresAt: this.getDefaultExpirationTime(), | 
				
			|||
    userFullName: '', | 
				
			|||
  }; | 
				
			|||
  password = ''; | 
				
			|||
  isLoading = false; | 
				
			|||
  activeDid = ''; | 
				
			|||
  apiServer = ''; | 
				
			|||
  isDeleting = false; | 
				
			|||
  showDeleteConfirm = false; | 
				
			|||
 | 
				
			|||
  get minDateTime() { | 
				
			|||
    const now = new Date(); | 
				
			|||
    now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future | 
				
			|||
    return this.formatDateForInput(now); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async created() { | 
				
			|||
    const settings = await retrieveSettingsForActiveAccount(); | 
				
			|||
    this.activeDid = settings.activeDid || ''; | 
				
			|||
    this.apiServer = settings.apiServer || ''; | 
				
			|||
    this.currentMeeting.userFullName = settings.firstName || ''; | 
				
			|||
     | 
				
			|||
    await this.fetchExistingMeeting(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  getDefaultExpirationTime(): string { | 
				
			|||
    const date = new Date(); | 
				
			|||
    // Round up to the next hour | 
				
			|||
    date.setMinutes(0); | 
				
			|||
    date.setSeconds(0); | 
				
			|||
    date.setMilliseconds(0); | 
				
			|||
    date.setHours(date.getHours() + 1); // Round up to next hour | 
				
			|||
    date.setHours(date.getHours() + 2); // Add 2 more hours | 
				
			|||
    return this.formatDateForInput(date); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  // Format a date object to YYYY-MM-DDTHH:mm format for datetime-local input | 
				
			|||
  private formatDateForInput(date: Date): string { | 
				
			|||
    const year = date.getFullYear(); | 
				
			|||
    const month = String(date.getMonth() + 1).padStart(2, '0'); | 
				
			|||
    const day = String(date.getDate()).padStart(2, '0'); | 
				
			|||
    const hours = String(date.getHours()).padStart(2, '0'); | 
				
			|||
    const minutes = String(date.getMinutes()).padStart(2, '0'); | 
				
			|||
     | 
				
			|||
    return `${year}-${month}-${day}T${hours}:${minutes}`; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async fetchExistingMeeting() { | 
				
			|||
    try { | 
				
			|||
      const headers = await getHeaders(this.activeDid); | 
				
			|||
      const response = await this.axios.get( | 
				
			|||
        this.apiServer + '/api/partner/groupOnboard', | 
				
			|||
        { headers } | 
				
			|||
      ); | 
				
			|||
       | 
				
			|||
      if (response.data && response.data.data) { | 
				
			|||
        this.existingMeeting = response.data.data; | 
				
			|||
      } | 
				
			|||
    } catch (error) { | 
				
			|||
      console.log('Error fetching existing meeting:', error); | 
				
			|||
      this.$notify( | 
				
			|||
        { | 
				
			|||
          group: 'alert', | 
				
			|||
          type: 'danger', | 
				
			|||
          title: 'Error', | 
				
			|||
          text: serverMessageForUser(error) || 'Failed to fetch existing meeting.', | 
				
			|||
        }, | 
				
			|||
      ); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async createMeeting() { | 
				
			|||
    this.isLoading = true; | 
				
			|||
 | 
				
			|||
    try { | 
				
			|||
      // Convert local time to UTC for comparison and server submission | 
				
			|||
      const localExpiresAt = new Date(this.currentMeeting.expiresAt); | 
				
			|||
      const now = new Date(); | 
				
			|||
      if (localExpiresAt <= now) { | 
				
			|||
        this.$notify( | 
				
			|||
          { | 
				
			|||
            group: 'alert', | 
				
			|||
            type: 'warning', | 
				
			|||
            title: 'Invalid Time', | 
				
			|||
            text: 'Select a future time for the meeting expiration.', | 
				
			|||
          }, | 
				
			|||
          5000 | 
				
			|||
        ); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // create content with user's name and DID encrypted with password | 
				
			|||
      const content = { | 
				
			|||
        name: this.currentMeeting.userFullName, | 
				
			|||
        did: this.activeDid, | 
				
			|||
      }; | 
				
			|||
      const encryptedContent = await encryptMessage(JSON.stringify(content), this.password); | 
				
			|||
     | 
				
			|||
      const headers = await getHeaders(this.activeDid); | 
				
			|||
      const response = await this.axios.post( | 
				
			|||
        this.apiServer + '/api/partner/groupOnboard', | 
				
			|||
        { | 
				
			|||
          name: this.currentMeeting.name, | 
				
			|||
          expiresAt: localExpiresAt.toISOString(), | 
				
			|||
          content: encryptedContent, | 
				
			|||
        }, | 
				
			|||
        { headers } | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      if (response.data && response.data.success) { | 
				
			|||
        this.existingMeeting = { | 
				
			|||
          groupId: response.data.groupId, | 
				
			|||
          name: this.currentMeeting.name, | 
				
			|||
          expiresAt: localExpiresAt.toISOString(), | 
				
			|||
        }; | 
				
			|||
        this.$notify( | 
				
			|||
          { | 
				
			|||
            group: 'alert', | 
				
			|||
            type: 'success', | 
				
			|||
            title: 'Success', | 
				
			|||
            text: 'Meeting created.', | 
				
			|||
          }, | 
				
			|||
          3000 | 
				
			|||
        ); | 
				
			|||
      } else { | 
				
			|||
        throw new Error('Failed to create meeting due to unexpected response data: ' + JSON.stringify(response.data)); | 
				
			|||
      } | 
				
			|||
    } catch (error) { | 
				
			|||
      console.error('Error creating meeting:', error); | 
				
			|||
      const errorMessage = serverMessageForUser(error); | 
				
			|||
      this.$notify( | 
				
			|||
        { | 
				
			|||
          group: 'alert', | 
				
			|||
          type: 'danger', | 
				
			|||
          title: 'Error', | 
				
			|||
          text: errorMessage || 'Failed to create meeting. Try reloading or submitting again.', | 
				
			|||
        }, | 
				
			|||
        5000 | 
				
			|||
      ); | 
				
			|||
    } finally { | 
				
			|||
      this.isLoading = false; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  formatExpirationTime(expiresAt: string): string { | 
				
			|||
    const expiration = new Date(expiresAt); // Server time is in UTC | 
				
			|||
    const now = new Date(); | 
				
			|||
    const diffHours = Math.round((expiration.getTime() - now.getTime()) / (1000 * 60 * 60)); | 
				
			|||
     | 
				
			|||
    if (diffHours < 0) { | 
				
			|||
      return 'Expired'; | 
				
			|||
    } else if (diffHours < 1) { | 
				
			|||
      return 'Less than an hour'; | 
				
			|||
    } else if (diffHours === 1) { | 
				
			|||
      return '1 hour'; | 
				
			|||
    } else { | 
				
			|||
      return `${diffHours} hours`; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  confirmDelete() { | 
				
			|||
    this.showDeleteConfirm = true; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async deleteMeeting() { | 
				
			|||
    this.isDeleting = true; | 
				
			|||
    try { | 
				
			|||
      const headers = await getHeaders(this.activeDid); | 
				
			|||
      await this.axios.delete( | 
				
			|||
        this.apiServer + '/api/partner/groupOnboard', | 
				
			|||
        { headers } | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      this.existingMeeting = null; | 
				
			|||
      this.showDeleteConfirm = false; | 
				
			|||
       | 
				
			|||
      this.$notify( | 
				
			|||
        { | 
				
			|||
          group: 'alert', | 
				
			|||
          type: 'success', | 
				
			|||
          title: 'Success', | 
				
			|||
          text: 'Meeting deleted successfully.', | 
				
			|||
        }, | 
				
			|||
        3000 | 
				
			|||
      ); | 
				
			|||
    } catch (error) { | 
				
			|||
      console.error('Error deleting meeting:', error); | 
				
			|||
      this.$notify( | 
				
			|||
        { | 
				
			|||
          group: 'alert', | 
				
			|||
          type: 'danger', | 
				
			|||
          title: 'Error', | 
				
			|||
          text: serverMessageForUser(error) || 'Failed to delete meeting.', | 
				
			|||
        }, | 
				
			|||
        5000 | 
				
			|||
      ); | 
				
			|||
    } finally { | 
				
			|||
      this.isDeleting = false; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
</script>  | 
				
			|||
					Loading…
					
					
				
		Reference in new issue