Files
crowd-funder-from-jason/src/components/MeetingExclusionGroups.vue

280 lines
7.9 KiB
Vue

<template>
<div>
<div v-for="group in groups" :key="group.id" class="mb-3">
<div
:class="[
'rounded-lg border p-3',
colorSet(group.colorIndex).bg,
colorSet(group.colorIndex).border,
]"
>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 flex-1 min-w-0">
<span
:class="[
'w-3 h-3 rounded-full shrink-0',
colorSet(group.colorIndex).dot,
]"
></span>
<input
:value="group.name"
:disabled="disabled"
:class="[
'text-sm font-medium bg-transparent border-none',
'outline-none flex-1 min-w-0 placeholder-gray-400',
{ 'cursor-default': disabled },
]"
placeholder="Group name…"
@input="
updateGroupName(
group.id,
($event.target as HTMLInputElement).value,
)
"
/>
</div>
<button
:class="[
'transition-colors ml-2 shrink-0',
disabled
? 'text-slate-300 cursor-not-allowed'
: 'text-slate-400 hover:text-red-600',
]"
title="Delete group"
@click="disabled ? notifyLocked() : removeGroup(group.id)"
>
<font-awesome icon="trash-can" class="text-sm" />
</button>
</div>
<div class="flex flex-wrap gap-1.5 mb-2">
<span
v-for="did in group.memberDids"
:key="did"
:class="[
'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs',
colorSet(group.colorIndex).chip,
]"
>
{{ getMemberName(did) }}
<button
:class="
disabled ? 'opacity-40 cursor-not-allowed' : 'hover:opacity-70'
"
@click="
disabled ? notifyLocked() : removeMemberFromGroup(group.id, did)
"
>
<font-awesome icon="xmark" class="text-xs" />
</button>
</span>
<span
v-if="group.memberDids.length === 0"
class="text-xs text-slate-400 italic"
>
No members yet
</span>
</div>
<div v-if="!disabled && addingToGroupId === group.id" class="mt-2">
<div
class="flex flex-wrap gap-1.5 p-2 bg-white bg-opacity-60 rounded border border-gray-200"
>
<button
v-for="member in availableMembersForGroup(group)"
:key="member.did"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-white border border-gray-300 hover:bg-gray-100 transition-colors"
@click="addMemberToGroup(group.id, member.did)"
>
<font-awesome icon="plus" class="text-xs text-green-600" />
{{ member.name }}
</button>
<span
v-if="availableMembersForGroup(group).length === 0"
class="text-xs text-slate-400 italic"
>
All members already assigned
</span>
</div>
<button
class="text-xs text-slate-500 mt-1"
@click="addingToGroupId = ''"
>
Done
</button>
</div>
<button
v-else
:class="[
'text-xs transition-colors',
disabled
? 'text-slate-400 cursor-not-allowed'
: 'text-blue-600 hover:text-blue-800',
]"
@click="disabled ? notifyLocked() : (addingToGroupId = group.id)"
>
<font-awesome icon="plus" class="text-xs" />
Add member
</button>
</div>
</div>
<button
:class="[
'text-sm transition-colors',
disabled
? 'text-slate-400 cursor-not-allowed'
: 'text-blue-600 hover:text-blue-800',
]"
@click="disabled ? notifyLocked() : addGroup()"
>
<font-awesome icon="plus" class="text-sm" />
New Group
</button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import { DoNotPairGroup } from "@/interfaces";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NotificationIface } from "@/constants/app";
interface MemberInfo {
did: string;
name: string;
}
const GROUP_COLORS = [
{
bg: "bg-orange-50",
border: "border-orange-200",
dot: "bg-orange-400",
chip: "bg-orange-200 text-orange-800",
},
{
bg: "bg-purple-50",
border: "border-purple-200",
dot: "bg-purple-400",
chip: "bg-purple-200 text-purple-800",
},
{
bg: "bg-teal-50",
border: "border-teal-200",
dot: "bg-teal-400",
chip: "bg-teal-200 text-teal-800",
},
{
bg: "bg-pink-50",
border: "border-pink-200",
dot: "bg-pink-400",
chip: "bg-pink-200 text-pink-800",
},
{
bg: "bg-indigo-50",
border: "border-indigo-200",
dot: "bg-indigo-400",
chip: "bg-indigo-200 text-indigo-800",
},
{
bg: "bg-yellow-50",
border: "border-yellow-200",
dot: "bg-yellow-400",
chip: "bg-yellow-200 text-yellow-800",
},
];
@Component
export default class MeetingExclusionGroups extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
notify!: ReturnType<typeof createNotifyHelpers>;
@Prop({ required: true }) groups!: DoNotPairGroup[];
@Prop({ required: true }) availableMembers!: MemberInfo[];
@Prop({ default: false }) disabled!: boolean;
addingToGroupId = "";
created() {
this.notify = createNotifyHelpers(this.$notify);
}
notifyLocked(): void {
this.notify.warning(
"Erase the current matches before changing exclusion groups.",
TIMEOUTS.LONG,
);
}
colorSet(colorIndex: number): (typeof GROUP_COLORS)[0] {
return GROUP_COLORS[colorIndex % GROUP_COLORS.length];
}
getMemberName(did: string): string {
const member = this.availableMembers.find((m) => m.did === did);
return member?.name || did.substring(0, 16) + "…";
}
availableMembersForGroup(group: DoNotPairGroup): MemberInfo[] {
const allAssignedDids = new Set(this.groups.flatMap((g) => g.memberDids));
return this.availableMembers
.filter(
(m) => !allAssignedDids.has(m.did) || group.memberDids.includes(m.did),
)
.filter((m) => !group.memberDids.includes(m.did));
}
@Emit("update")
emitUpdate(): DoNotPairGroup[] {
return [...this.groups];
}
addGroup(): void {
const newGroup: DoNotPairGroup = {
id: Date.now().toString(36) + Math.random().toString(36).substring(2, 6),
name: "",
colorIndex: this.groups.length % GROUP_COLORS.length,
memberDids: [],
};
this.groups.push(newGroup);
this.addingToGroupId = newGroup.id;
this.emitUpdate();
}
removeGroup(groupId: string): void {
const idx = this.groups.findIndex((g) => g.id === groupId);
if (idx !== -1) {
this.groups.splice(idx, 1);
if (this.addingToGroupId === groupId) {
this.addingToGroupId = "";
}
this.emitUpdate();
}
}
updateGroupName(groupId: string, name: string): void {
const group = this.groups.find((g) => g.id === groupId);
if (group) {
group.name = name;
this.emitUpdate();
}
}
addMemberToGroup(groupId: string, did: string): void {
const group = this.groups.find((g) => g.id === groupId);
if (group && !group.memberDids.includes(did)) {
group.memberDids.push(did);
this.emitUpdate();
}
}
removeMemberFromGroup(groupId: string, did: string): void {
const group = this.groups.find((g) => g.id === groupId);
if (group) {
group.memberDids = group.memberDids.filter((d) => d !== did);
this.emitUpdate();
}
}
}
</script>