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>
|
|
<div v-if="visible" class="dialog-overlay">
|
|
<div class="dialog">
|
|
<div class="text-slate-900 text-center">
|
|
<h3 class="text-lg font-semibold leading-[1.25] mb-2">
|
|
{{ title }}
|
|
</h3>
|
|
<p class="text-sm mb-4">
|
|
{{ description }}
|
|
</p>
|
|
|
|
<!-- Member Selection Table -->
|
|
<div class="mb-4">
|
|
<table
|
|
class="w-full border-collapse border border-slate-300 text-sm text-start"
|
|
>
|
|
<!-- Select All Header -->
|
|
<thead v-if="membersData && membersData.length > 0">
|
|
<tr class="bg-slate-100 font-medium">
|
|
<th class="border border-slate-300 px-3 py-2">
|
|
<label class="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
:checked="isAllSelected"
|
|
:indeterminate="isIndeterminate"
|
|
@change="toggleSelectAll"
|
|
/>
|
|
Select All
|
|
</label>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Empty State -->
|
|
<tr v-if="!membersData || membersData.length === 0">
|
|
<td
|
|
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
|
|
>
|
|
{{ emptyStateText }}
|
|
</td>
|
|
</tr>
|
|
<!-- Member Rows -->
|
|
<tr
|
|
v-for="member in membersData || []"
|
|
:key="member.member.memberId"
|
|
>
|
|
<td class="border border-slate-300 px-3 py-2">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<label class="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
:checked="isMemberSelected(member.did)"
|
|
@change="toggleMemberSelection(member.did)"
|
|
/>
|
|
<div class="">
|
|
<div class="text-sm font-semibold">
|
|
{{ member.name || SOMEONE_UNNAMED }}
|
|
</div>
|
|
<div
|
|
class="flex items-center gap-0.5 text-xs text-slate-500"
|
|
>
|
|
<span class="font-semibold sm:hidden">DID:</span>
|
|
<span
|
|
class="w-[35vw] sm:w-auto truncate text-left"
|
|
style="direction: rtl"
|
|
>{{ member.did }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
|
|
<!-- Contact indicator - only show if they are already a contact -->
|
|
<font-awesome
|
|
v-if="member.isContact"
|
|
icon="user-circle"
|
|
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
|
|
@click="showContactInfo"
|
|
/>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
<!-- Select All Footer -->
|
|
<tfoot v-if="membersData && membersData.length > 0">
|
|
<tr class="bg-slate-100 font-medium">
|
|
<th class="border border-slate-300 px-3 py-2">
|
|
<label class="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
:checked="isAllSelected"
|
|
:indeterminate="isIndeterminate"
|
|
@change="toggleSelectAll"
|
|
/>
|
|
Select All
|
|
</label>
|
|
</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="space-y-2">
|
|
<!-- Main Action Button -->
|
|
<button
|
|
v-if="membersData && membersData.length > 0"
|
|
:disabled="!hasSelectedMembers"
|
|
:class="[
|
|
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
|
|
hasSelectedMembers
|
|
? 'bg-blue-600 text-white cursor-pointer'
|
|
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
|
|
]"
|
|
@click="handleMainAction"
|
|
>
|
|
{{ buttonText }}
|
|
</button>
|
|
<!-- Cancel Button -->
|
|
<button
|
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
|
@click="cancel"
|
|
>
|
|
Maybe Later
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
|
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
|
import { MemberData } from "@/interfaces";
|
|
import { setVisibilityUtil, getHeaders, register } from "@/libs/endorserServer";
|
|
import { createNotifyHelpers } from "@/utils/notify";
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
@Component({
|
|
mixins: [PlatformServiceMixin],
|
|
emits: ["close"],
|
|
})
|
|
export default class BulkMembersDialog extends Vue {
|
|
@Prop({ default: "" }) activeDid!: string;
|
|
@Prop({ default: "" }) apiServer!: string;
|
|
// isOrganizer: true = organizer mode (admit members), false = member mode (set visibility)
|
|
@Prop({ required: true }) isOrganizer!: boolean;
|
|
|
|
// Vue notification system
|
|
$notify!: (
|
|
notification: { group: string; type: string; title: string; text: string },
|
|
timeout?: number,
|
|
) => void;
|
|
|
|
// Notification system
|
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
// Component state
|
|
membersData: MemberData[] = [];
|
|
selectedMembers: string[] = [];
|
|
visible = false;
|
|
|
|
// Constants
|
|
// In Vue templates, imported constants need to be explicitly made available to the template
|
|
readonly SOMEONE_UNNAMED = SOMEONE_UNNAMED;
|
|
|
|
get hasSelectedMembers() {
|
|
return this.selectedMembers.length > 0;
|
|
}
|
|
|
|
get isAllSelected() {
|
|
if (!this.membersData || this.membersData.length === 0) return false;
|
|
return this.membersData.every((member) =>
|
|
this.selectedMembers.includes(member.did),
|
|
);
|
|
}
|
|
|
|
get isIndeterminate() {
|
|
if (!this.membersData || this.membersData.length === 0) return false;
|
|
const selectedCount = this.membersData.filter((member) =>
|
|
this.selectedMembers.includes(member.did),
|
|
).length;
|
|
return selectedCount > 0 && selectedCount < this.membersData.length;
|
|
}
|
|
|
|
get title() {
|
|
return this.isOrganizer
|
|
? "Admit Pending Members"
|
|
: "Add Members to Contacts";
|
|
}
|
|
|
|
get description() {
|
|
return this.isOrganizer
|
|
? "Would you like to admit these members to the meeting and add them to your contacts?"
|
|
: "Would you like to add these members to your contacts?";
|
|
}
|
|
|
|
get buttonText() {
|
|
return this.isOrganizer ? "Admit + Add to Contacts" : "Add to Contacts";
|
|
}
|
|
|
|
get emptyStateText() {
|
|
return this.isOrganizer
|
|
? "No pending members to admit"
|
|
: "No members are not in your contacts";
|
|
}
|
|
|
|
created() {
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
}
|
|
|
|
open(members: MemberData[]) {
|
|
this.visible = true;
|
|
this.membersData = members;
|
|
// Select all by default
|
|
this.selectedMembers = this.membersData.map((member) => member.did);
|
|
}
|
|
|
|
close(notSelectedMemberDids: string[]) {
|
|
this.visible = false;
|
|
this.$emit("close", { notSelectedMemberDids: notSelectedMemberDids });
|
|
}
|
|
|
|
cancel() {
|
|
this.close(this.membersData.map((member) => member.did));
|
|
}
|
|
|
|
toggleSelectAll() {
|
|
if (!this.membersData || this.membersData.length === 0) return;
|
|
|
|
if (this.isAllSelected) {
|
|
// Deselect all
|
|
this.selectedMembers = [];
|
|
} else {
|
|
// Select all
|
|
this.selectedMembers = this.membersData.map((member) => member.did);
|
|
}
|
|
}
|
|
|
|
toggleMemberSelection(memberDid: string) {
|
|
const index = this.selectedMembers.indexOf(memberDid);
|
|
if (index > -1) {
|
|
this.selectedMembers.splice(index, 1);
|
|
} else {
|
|
this.selectedMembers.push(memberDid);
|
|
}
|
|
}
|
|
|
|
isMemberSelected(memberDid: string) {
|
|
return this.selectedMembers.includes(memberDid);
|
|
}
|
|
|
|
async handleMainAction() {
|
|
// isOrganizer: true = admit mode, false = visibility mode
|
|
if (this.isOrganizer) {
|
|
await this.organizerAdmitAndAddWithVisibility();
|
|
} else {
|
|
await this.memberAddContactWithVisibility();
|
|
}
|
|
}
|
|
|
|
async organizerAdmitAndAddWithVisibility() {
|
|
try {
|
|
const selectedMembers: MemberData[] = this.membersData.filter((member) =>
|
|
this.selectedMembers.includes(member.did),
|
|
);
|
|
const notSelectedMembers: MemberData[] = this.membersData.filter(
|
|
(member) => !this.selectedMembers.includes(member.did),
|
|
);
|
|
|
|
let admittedCount = 0;
|
|
let contactAddedCount = 0;
|
|
let errors = 0;
|
|
|
|
for (const member of selectedMembers) {
|
|
try {
|
|
// First, admit the member
|
|
await this.admitMember(member);
|
|
|
|
// Register them
|
|
await this.registerMember(member);
|
|
admittedCount++;
|
|
|
|
// If they're not a contact yet, add them as a contact
|
|
if (!member.isContact) {
|
|
await this.addAsContact(member, true);
|
|
contactAddedCount++;
|
|
}
|
|
|
|
// Set their seesMe to true
|
|
await this.updateContactVisibility(member.did, true);
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`Error processing member ${member.did}:`, error);
|
|
// Continue with other members even if one fails
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
// Show success notification
|
|
if (admittedCount > 0) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Members Admitted Successfully",
|
|
text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted and registered${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`,
|
|
},
|
|
10000,
|
|
);
|
|
}
|
|
if (errors > 0) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to fully admit some members. Work with them individually below.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
this.close(notSelectedMembers.map((member) => member.did));
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Error admitting members:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Some errors occurred. Work with members individually below.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
async memberAddContactWithVisibility() {
|
|
try {
|
|
const selectedMembers: MemberData[] = this.membersData.filter((member) =>
|
|
this.selectedMembers.includes(member.did),
|
|
);
|
|
const notSelectedMembers: MemberData[] = this.membersData.filter(
|
|
(member) => !this.selectedMembers.includes(member.did),
|
|
);
|
|
|
|
let contactsAddedCount = 0;
|
|
|
|
for (const member of selectedMembers) {
|
|
try {
|
|
// If they're not a contact yet, add them as a contact first
|
|
if (!member.isContact) {
|
|
await this.addAsContact(member, undefined);
|
|
contactsAddedCount++;
|
|
}
|
|
|
|
// Set their seesMe to true
|
|
await this.updateContactVisibility(member.did, true);
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`Error processing member ${member.did}:`, error);
|
|
// Continue with other members even if one fails
|
|
}
|
|
}
|
|
|
|
// Show success notification
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Contacts Added Successfully",
|
|
text: `${contactsAddedCount} member${contactsAddedCount === 1 ? "" : "s"} added as contact${contactsAddedCount === 1 ? "" : "s"}.`,
|
|
},
|
|
5000,
|
|
);
|
|
|
|
this.close(notSelectedMembers.map((member) => member.did));
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Error adding contacts:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Some errors occurred. Work with members individually below.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
async admitMember(member: {
|
|
did: string;
|
|
name: string;
|
|
member: { memberId: string };
|
|
}) {
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
await this.axios.put(
|
|
`${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`,
|
|
{ admitted: true },
|
|
{ headers },
|
|
);
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Error admitting member:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async registerMember(member: MemberData) {
|
|
try {
|
|
const contact: Contact = { did: member.did };
|
|
const result = await register(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
);
|
|
if (result.success) {
|
|
if (result.embeddedRecordError) {
|
|
throw new Error(result.embeddedRecordError);
|
|
}
|
|
await this.$updateContact(member.did, { registered: true });
|
|
} else {
|
|
throw result;
|
|
}
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Error registering member:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async addAsContact(
|
|
member: { did: string; name: string },
|
|
isRegistered?: boolean,
|
|
) {
|
|
try {
|
|
const newContact: Contact = {
|
|
did: member.did,
|
|
name: member.name,
|
|
registered: isRegistered,
|
|
};
|
|
|
|
await this.$insertContact(newContact);
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Error adding contact:", err);
|
|
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
|
|
// Contact already exists, continue
|
|
} else {
|
|
throw err; // Re-throw if it's not a duplicate error
|
|
}
|
|
}
|
|
}
|
|
|
|
async updateContactVisibility(did: string, seesMe: boolean) {
|
|
try {
|
|
// Get the contact object
|
|
const contact = await this.$getContact(did);
|
|
if (!contact) {
|
|
throw new Error(`Contact not found for DID: ${did}`);
|
|
}
|
|
|
|
// Use the proper API to set visibility on the server
|
|
const result = await setVisibilityUtil(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
seesMe,
|
|
);
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || "Failed to set visibility");
|
|
}
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("Error updating contact visibility:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
showContactInfo() {
|
|
// isOrganizer: true = admit mode, false = visibility mode
|
|
const message = this.isOrganizer
|
|
? "This user is already your contact, but they are not yet admitted to the meeting."
|
|
: "This user is already your contact, but your activities are not visible to them yet.";
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Contact Info",
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
</script>
|
|
|