forked from jsnbuchanan/crowd-funder-for-time-pwa
280 lines
7.9 KiB
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>
|