203 lines
9.5 KiB
Vue
203 lines
9.5 KiB
Vue
<script setup lang="ts">
|
|
type PeopleListMode = "contacts" | "deals";
|
|
|
|
defineProps<{
|
|
peopleListMode: PeopleListMode;
|
|
peopleSearch: string;
|
|
peopleSortOptions: Array<{ value: string; label: string }>;
|
|
peopleSortMode: string;
|
|
peopleContactList: any[];
|
|
selectedCommThreadId: string;
|
|
isReviewHighlightedContact: (contactId: string) => boolean;
|
|
openCommunicationThread: (contactName: string) => void;
|
|
avatarSrcForThread: (thread: any) => string;
|
|
markAvatarBroken: (threadId: string) => void;
|
|
contactInitials: (contactName: string) => string;
|
|
formatThreadTime: (iso: string) => string;
|
|
threadInboxes: (thread: any) => any[];
|
|
setInboxHidden: (inboxId: string, hidden: boolean) => void;
|
|
formatInboxLabel: (inbox: any) => string;
|
|
isInboxToggleLoading: (inboxId: string) => boolean;
|
|
peopleDealList: any[];
|
|
selectedDealId: string;
|
|
isReviewHighlightedDeal: (dealId: string) => boolean;
|
|
openDealThread: (deal: any) => void;
|
|
getDealCurrentStepLabel: (deal: any) => string;
|
|
onPeopleListModeChange: (mode: PeopleListMode) => void;
|
|
onPeopleSearchInput: (value: string) => void;
|
|
onPeopleSortModeChange: (mode: string) => void;
|
|
}>();
|
|
|
|
function onSearchInput(event: Event) {
|
|
const target = event.target as HTMLInputElement | null;
|
|
onPeopleSearchInput(target?.value ?? "");
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<aside class="h-full min-h-0 border-r border-base-300 flex flex-col md:row-span-2">
|
|
<div class="sticky top-0 z-20 h-12 border-b border-base-300 bg-base-100 px-2">
|
|
<div class="flex h-full items-center gap-1">
|
|
<div class="join rounded-lg border border-base-300 overflow-hidden">
|
|
<button
|
|
class="btn btn-ghost btn-sm join-item rounded-none"
|
|
:class="peopleListMode === 'contacts' ? 'bg-base-200/80 text-base-content' : 'text-base-content/65 hover:text-base-content'"
|
|
title="Contacts"
|
|
@click="onPeopleListModeChange('contacts')"
|
|
>
|
|
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
|
<path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5m0 2c-4.42 0-8 2.24-8 5v1h16v-1c0-2.76-3.58-5-8-5" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
class="btn btn-ghost btn-sm join-item rounded-none border-l border-base-300/70"
|
|
:class="peopleListMode === 'deals' ? 'bg-base-200/80 text-base-content' : 'text-base-content/65 hover:text-base-content'"
|
|
title="Deals"
|
|
@click="onPeopleListModeChange('deals')"
|
|
>
|
|
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
|
<path d="M10 3h4a2 2 0 0 1 2 2v2h3a2 2 0 0 1 2 2v3H3V9a2 2 0 0 1 2-2h3V5a2 2 0 0 1 2-2m0 4h4V5h-4zm11 7v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5h7v2h4v-2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<input
|
|
:value="peopleSearch"
|
|
type="text"
|
|
class="input input-bordered input-sm w-full"
|
|
:placeholder="peopleListMode === 'contacts' ? 'Search contacts' : 'Search deals'"
|
|
@input="onSearchInput"
|
|
>
|
|
|
|
<div class="dropdown dropdown-end">
|
|
<button
|
|
tabindex="0"
|
|
class="btn btn-ghost btn-sm btn-square"
|
|
:title="peopleListMode === 'contacts' ? 'Sort contacts' : 'Sort deals'"
|
|
>
|
|
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
|
<path d="M3 5h18v2H3zm3 6h12v2H6zm4 6h4v2h-4z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div tabindex="0" class="dropdown-content z-20 mt-2 w-52 rounded-xl border border-base-300 bg-base-100 p-2 shadow-lg">
|
|
<template v-if="peopleListMode === 'contacts'">
|
|
<p class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-base-content/55">Sort contacts</p>
|
|
<button
|
|
v-for="option in peopleSortOptions"
|
|
:key="`people-sort-${option.value}`"
|
|
class="btn btn-ghost btn-sm w-full justify-between"
|
|
@click="onPeopleSortModeChange(option.value)"
|
|
>
|
|
<span>{{ option.label }}</span>
|
|
<span v-if="peopleSortMode === option.value">✓</span>
|
|
</button>
|
|
</template>
|
|
<p v-else class="px-2 py-1 text-xs text-base-content/60">Deals are sorted by title.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="min-h-0 flex-1 overflow-y-auto p-0">
|
|
<div
|
|
v-if="peopleListMode === 'contacts'"
|
|
v-for="thread in peopleContactList"
|
|
:key="thread.id"
|
|
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
|
:class="[
|
|
selectedCommThreadId === thread.id ? 'bg-primary/10' : '',
|
|
isReviewHighlightedContact(thread.id) ? 'bg-primary/10 ring-1 ring-primary/45' : '',
|
|
]"
|
|
@click="openCommunicationThread(thread.contact)"
|
|
role="button"
|
|
tabindex="0"
|
|
@keydown.enter.prevent="openCommunicationThread(thread.contact)"
|
|
@keydown.space.prevent="openCommunicationThread(thread.contact)"
|
|
>
|
|
<div class="flex items-start gap-2">
|
|
<div class="avatar shrink-0">
|
|
<div class="h-8 w-8 rounded-full ring-1 ring-base-300/70">
|
|
<img
|
|
v-if="avatarSrcForThread(thread)"
|
|
:src="avatarSrcForThread(thread)"
|
|
:alt="thread.contact"
|
|
@error="markAvatarBroken(thread.id)"
|
|
>
|
|
<span v-else class="flex h-full w-full items-center justify-center text-[10px] font-semibold text-base-content/65">
|
|
{{ contactInitials(thread.contact) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-1.5">
|
|
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ thread.contact }}</p>
|
|
<span class="shrink-0 text-[10px] text-base-content/55">{{ formatThreadTime(thread.lastAt) }}</span>
|
|
<div class="dropdown dropdown-end shrink-0" @click.stop>
|
|
<button
|
|
tabindex="0"
|
|
class="btn btn-ghost btn-xs btn-square h-5 min-h-5"
|
|
title="Source visibility settings"
|
|
>
|
|
<svg viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
|
<path d="M19.14 12.94a7.43 7.43 0 0 0 .05-.94 7.43 7.43 0 0 0-.05-.94l2.03-1.58a.5.5 0 0 0 .12-.63l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.2 7.2 0 0 0-1.62-.94l-.36-2.54A.5.5 0 0 0 13.9 2h-3.8a.5.5 0 0 0-.49.41L9.25 4.95a7.2 7.2 0 0 0-1.62.94l-2.39-.96a.5.5 0 0 0-.6.22L2.72 8.47a.5.5 0 0 0 .12.63l2.03 1.58a7.43 7.43 0 0 0-.05.94c0 .31.02.63.05.94l-2.03 1.58a.5.5 0 0 0-.12.63l1.92 3.32c.13.23.39.32.6.22l2.39-.96c.5.39 1.05.71 1.62.94l.36 2.54c.04.24.25.41.49.41h3.8c.24 0 .45-.17.49-.41l.36-2.54c.57-.23 1.12-.55 1.62-.94l2.39.96c.22.09.47 0 .6-.22l1.92-3.32a.5.5 0 0 0-.12-.63zM12 15.5A3.5 3.5 0 1 1 12 8a3.5 3.5 0 0 1 0 7.5Z" />
|
|
</svg>
|
|
</button>
|
|
<div tabindex="0" class="dropdown-content z-20 mt-1 w-60 rounded-xl border border-base-300 bg-base-100 p-2 shadow-lg">
|
|
<p class="px-1 pb-1 text-[10px] font-semibold uppercase tracking-wide text-base-content/55">Sources</p>
|
|
<div v-if="threadInboxes(thread).length" class="space-y-1">
|
|
<button
|
|
v-for="inbox in threadInboxes(thread)"
|
|
:key="`thread-inbox-setting-${inbox.id}`"
|
|
class="btn btn-ghost btn-xs h-auto min-h-0 w-full justify-between px-2 py-1 text-left normal-case"
|
|
@click.stop="setInboxHidden(inbox.id, !inbox.isHidden)"
|
|
>
|
|
<span class="min-w-0 truncate">{{ formatInboxLabel(inbox) }}</span>
|
|
<span class="shrink-0 text-[10px] text-base-content/70">
|
|
{{
|
|
isInboxToggleLoading(inbox.id)
|
|
? "..."
|
|
: inbox.isHidden
|
|
? "Hidden"
|
|
: "Visible"
|
|
}}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<p v-else class="px-1 py-1 text-[11px] text-base-content/60">No sources.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
v-if="peopleListMode === 'deals'"
|
|
v-for="deal in peopleDealList"
|
|
:key="deal.id"
|
|
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
|
:class="[
|
|
selectedDealId === deal.id ? 'bg-primary/10' : '',
|
|
isReviewHighlightedDeal(deal.id) ? 'bg-primary/10 ring-1 ring-primary/45' : '',
|
|
]"
|
|
@click="openDealThread(deal)"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<p class="truncate text-xs font-semibold">{{ deal.title }}</p>
|
|
<span class="shrink-0 text-[10px] text-base-content/55">{{ deal.amount }}</span>
|
|
</div>
|
|
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ deal.company }} · {{ deal.stage }}</p>
|
|
<p class="mt-0.5 truncate text-[11px] text-base-content/60">{{ getDealCurrentStepLabel(deal) }}</p>
|
|
</button>
|
|
|
|
<p v-if="peopleListMode === 'contacts' && peopleContactList.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
|
No contacts found.
|
|
</p>
|
|
<p v-if="peopleListMode === 'deals' && peopleDealList.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
|
No deals found.
|
|
</p>
|
|
</div>
|
|
</aside>
|
|
</template>
|