refactor(workspace): extract communications sidebars

This commit is contained in:
Ruslan Bakiev
2026-02-23 11:44:53 +07:00
parent 82bc5dd04e
commit 8be6e7d581
3 changed files with 483 additions and 485 deletions

View File

@@ -3,6 +3,8 @@ import { nextTick, onBeforeUnmount, onMounted } from "vue";
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
import CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
import CrmCommunicationsContextSidebar from "~~/app/components/workspace/communications/CrmCommunicationsContextSidebar.vue";
import CrmCommunicationsListSidebar from "~~/app/components/workspace/communications/CrmCommunicationsListSidebar.vue";
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.vue";
@@ -4908,354 +4910,36 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
:calendar-zoom-ghost="calendarZoomGhost"
/>
<section v-else-if="selectedTab === 'communications' && false" class="space-y-3">
<div class="mb-1 flex justify-end">
<div class="join">
<button class="btn btn-sm join-item btn-primary" @click="setPeopleLeftMode('contacts', true)">Contacts</button>
<button class="btn btn-sm join-item btn-ghost" @click="setPeopleLeftMode('calendar', true)">Calendar</button>
</div>
</div>
<div class="rounded-xl border border-base-300 p-3">
<div class="flex flex-wrap items-center gap-2">
<input
v-model="contactSearch"
type="text"
class="input input-bordered input-md w-full flex-1"
placeholder="Search contacts..."
>
<select v-model="sortMode" class="select select-bordered select-sm w-40">
<option value="name">Sort: Name</option>
<option value="lastContact">Sort: Last contact</option>
</select>
<div class="dropdown dropdown-end">
<button tabindex="0" class="btn btn-sm btn-ghost btn-square" title="Filters">
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
<path d="M3 5h18v2H3zm4 6h10v2H7zm3 6h4v2h-4z" />
</svg>
</button>
<div tabindex="0" class="dropdown-content z-20 mt-2 w-72 rounded-xl border border-base-300 bg-base-100 p-3 shadow-lg">
<div class="grid gap-2">
<label class="form-control">
<span class="label-text text-xs uppercase tracking-wide text-base-content/60">Country</span>
<select v-model="selectedCountry" class="select select-bordered select-sm">
<option v-for="country in countries" :key="`country-${country}`" :value="country">{{ country }}</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-xs uppercase tracking-wide text-base-content/60">Location</span>
<select v-model="selectedLocation" class="select select-bordered select-sm">
<option v-for="location in locations" :key="`location-${location}`" :value="location">{{ location }}</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-xs uppercase tracking-wide text-base-content/60">Company</span>
<select v-model="selectedCompany" class="select select-bordered select-sm">
<option v-for="company in companies" :key="`company-${company}`" :value="company">{{ company }}</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-xs uppercase tracking-wide text-base-content/60">Channel</span>
<select v-model="selectedChannel" class="select select-bordered select-sm">
<option v-for="channel in channels" :key="`channel-${channel}`" :value="channel">{{ channel }}</option>
</select>
</label>
</div>
<div class="mt-3 flex justify-end">
<button class="btn btn-ghost btn-sm" @click="resetContactFilters">Reset filters</button>
</div>
</div>
</div>
</div>
</div>
<div class="grid gap-3 md:grid-cols-12">
<aside class="min-h-0 rounded-xl border border-base-300 md:col-span-4">
<div class="min-h-0 space-y-3 overflow-y-auto p-2">
<article v-for="group in groupedContacts" :key="group[0]" class="space-y-2">
<div class="sticky top-0 z-10 rounded-lg bg-base-200 px-3 py-1 text-sm font-semibold">{{ group[0] }}</div>
<button
v-for="contact in group[1]"
:key="contact.id"
class="w-full rounded-xl border border-base-300 p-3 text-left transition hover:bg-base-200/60"
:class="selectedContactId === contact.id ? 'border-primary bg-primary/5' : ''"
@click="selectedContactId = contact.id"
>
<p class="font-medium">{{ contact.name }}</p>
<p class="text-xs text-base-content/60">{{ contact.company }} · {{ contact.location }}, {{ contact.country }}</p>
<p class="mt-1 text-[11px] text-base-content/55">Last contact · {{ formatStamp(contact.lastContactAt) }}</p>
</button>
</article>
</div>
</aside>
<article class="min-h-0 rounded-xl border border-base-300 md:col-span-8">
<div v-if="selectedContact" class="p-3 md:p-4">
<div class="border-b border-base-300 pb-2">
<p class="font-medium">{{ selectedContact!.name }}</p>
<p class="text-xs text-base-content/60">
{{ selectedContact!.company }} · {{ selectedContact!.location }}, {{ selectedContact!.country }}
</p>
<p class="mt-1 text-xs text-base-content/55">Last contact · {{ formatStamp(selectedContact!.lastContactAt) }}</p>
</div>
<div class="mt-3">
<ContactCollaborativeEditor
:key="`contact-editor-${selectedContact!.id}`"
v-model="selectedContact!.description"
:room="`crm-contact-${selectedContact!.id}`"
placeholder="Describe contact context and next steps..."
/>
</div>
<div class="mt-4 grid gap-3 xl:grid-cols-2">
<section class="rounded-xl border border-base-300 p-3">
<div class="mb-2 flex items-center justify-between gap-2">
<p class="text-sm font-semibold">Upcoming events</p>
</div>
<div class="space-y-2">
<button
v-for="event in selectedContactEvents"
:key="`contact-event-${event.id}`"
class="w-full rounded-lg border border-base-300 px-3 py-2 text-left transition hover:bg-base-200/50"
:class="isReviewHighlightedEvent(event.id) ? 'border-success/60 bg-success/10' : ''"
@click="openEventFromContact(event)"
>
<p class="text-sm font-medium">{{ event.title }}</p>
<p class="text-xs text-base-content/65">
{{ formatDay(event.start) }} · {{ formatTime(event.start) }} - {{ formatTime(event.end) }}
</p>
<p class="mt-1 text-xs text-base-content/80">{{ event.note }}</p>
</button>
<p v-if="selectedContactEvents.length === 0" class="text-xs text-base-content/55">
No linked events yet.
</p>
</div>
</section>
<section class="rounded-xl border border-base-300 p-3">
<div class="mb-2 flex items-center justify-between gap-2">
<p class="text-sm font-semibold">Recent messages</p>
<button class="btn btn-ghost btn-xs" @click="openCommunicationThread(selectedContact!.name)">Open chat</button>
</div>
<div class="space-y-2">
<button
v-for="item in selectedContactRecentMessages"
:key="`contact-message-${item.id}`"
class="w-full rounded-lg border border-base-300 px-3 py-2 text-left transition hover:bg-base-200/50"
@click="openMessageFromContact(item.channel)"
>
<p class="text-sm text-base-content/90">{{ item.text }}</p>
<p class="mt-1 text-xs text-base-content/60">
<span class="mr-1 inline-flex h-4 w-4 items-center justify-center align-middle">
<svg v-if="channelIcon(item.channel) === 'telegram'" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M9.04 15.51 8.7 20.27c.49 0 .7-.21.96-.46l2.3-2.2 4.77 3.49c.88.49 1.5.23 1.74-.81l3.15-14.77.01-.01c.29-1.35-.49-1.88-1.35-1.56L1.74 11.08c-1.28.5-1.26 1.22-.22 1.54l4.74 1.48L17.3 7.03c.52-.34 1-.15.61.19" />
</svg>
<svg v-else-if="channelIcon(item.channel) === 'whatsapp'" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M20 3.99A9.94 9.94 0 0 0 12.01 1C6.49 1 2 5.49 2 11c0 1.76.46 3.49 1.33 5.03L2 23l7.17-1.88A9.95 9.95 0 0 0 12 21h.01c5.51 0 9.99-4.49 9.99-10.01 0-2.67-1.04-5.18-2.99-7m-7.99 15.32h-.01a8.3 8.3 0 0 1-4.23-1.16l-.3-.18-4.26 1.12 1.14-4.15-.2-.32a8.28 8.28 0 0 1-1.27-4.4c0-4.58 3.73-8.31 8.32-8.31a8.27 8.27 0 0 1 5.88 2.44 8.25 8.25 0 0 1 2.43 5.87c0 4.59-3.73 8.32-8.31 8.32m4.56-6.23c-.25-.12-1.49-.74-1.73-.82-.23-.09-.4-.12-.57.12s-.66.82-.81.99-.3.18-.55.06a6.7 6.7 0 0 1-1.97-1.21 7.43 7.43 0 0 1-1.38-1.71c-.14-.24-.01-.37.1-.49.11-.11.24-.3.36-.45.12-.14.16-.24.25-.39.08-.18.05-.3-.02-.42-.07-.12-.56-1.35-.77-1.85-.2-.48-.41-.41-.57-.42h-.48c-.16 0-.42.06-.64.3-.22.24-.84.82-.84 2s.86 2.31.98 2.48c.12.16 1.69 2.57 4.09 3.6.57.24 1.01.38 1.36.48.58.18 1.11.15 1.52.09.46-.06 1.49-.61 1.7-1.19.21-.58.21-1.09.15-1.19-.06-.11-.23-.17-.48-.3" />
</svg>
<svg v-else-if="channelIcon(item.channel) === 'instagram'" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M7 2h10a5 5 0 0 1 5 5v10a5 5 0 0 1-5 5H7a5 5 0 0 1-5-5V7a5 5 0 0 1 5-5m10 2H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3m-5 3.5A4.5 4.5 0 1 1 7.5 12 4.5 4.5 0 0 1 12 7.5m0 2A2.5 2.5 0 1 0 14.5 12 2.5 2.5 0 0 0 12 9.5m4.8-3.2a1.2 1.2 0 1 1-1.2 1.2 1.2 1.2 0 0 1 1.2-1.2" />
</svg>
<svg v-else-if="channelIcon(item.channel) === 'email'" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M20 4H4a2 2 0 0 0-2 2v.4l10 5.6 10-5.6V6a2 2 0 0 0-2-2m0 4.2-7.4 4.14a1.25 1.25 0 0 1-1.2 0L4 8.2V18a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2z" />
</svg>
<svg v-else viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M6.62 10.79a15.47 15.47 0 0 0 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1C10.07 21 3 13.93 3 5c0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.24.2 2.45.57 3.57.11.35.03.74-.25 1.02z" />
</svg>
</span>
<span>{{ formatStamp(item.at) }}</span>
</p>
</button>
<p v-if="selectedContactRecentMessages.length === 0" class="text-xs text-base-content/55">
No messages yet.
</p>
</div>
</section>
</div>
</div>
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
No contact selected.
</div>
</article>
</div>
</section>
<section v-else-if="selectedTab === 'communications' && peopleLeftMode === 'contacts'" class="flex h-full min-h-0 flex-col gap-0">
<div class="grid h-full min-h-0 flex-1 gap-0 md:grid-cols-[248px_minmax(0,1fr)_320px] md:grid-rows-[auto_minmax(0,1fr)]">
<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="peopleListMode = '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="peopleListMode = '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
v-model="peopleSearch"
type="text"
class="input input-bordered input-sm w-full"
:placeholder="peopleListMode === 'contacts' ? 'Search contacts' : 'Search deals'"
>
<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="peopleSortMode = 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-start justify-between gap-2">
<p class="truncate text-xs font-semibold">{{ thread.contact }}</p>
<span class="shrink-0 text-[10px] text-base-content/55">{{ formatThreadTime(thread.lastAt) }}</span>
</div>
<div class="mt-0.5 flex items-center justify-between gap-2">
<p class="min-w-0 flex-1 truncate text-[11px] text-base-content/75">{{ threadChannelLabel(thread) }}</p>
<div class="dropdown dropdown-end" @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>
<CrmCommunicationsListSidebar
:people-list-mode="peopleListMode"
:people-search="peopleSearch"
:people-sort-options="peopleSortOptions"
:people-sort-mode="peopleSortMode"
:people-contact-list="peopleContactList"
:selected-comm-thread-id="selectedCommThreadId"
:is-review-highlighted-contact="isReviewHighlightedContact"
:open-communication-thread="openCommunicationThread"
:avatar-src-for-thread="avatarSrcForThread"
:mark-avatar-broken="markAvatarBroken"
:contact-initials="contactInitials"
:format-thread-time="formatThreadTime"
:thread-channel-label="threadChannelLabel"
:thread-inboxes="threadInboxes"
:set-inbox-hidden="setInboxHidden"
:format-inbox-label="formatInboxLabel"
:is-inbox-toggle-loading="isInboxToggleLoading"
:people-deal-list="peopleDealList"
:selected-deal-id="selectedDealId"
:is-review-highlighted-deal="isReviewHighlightedDeal"
:open-deal-thread="openDealThread"
:get-deal-current-step-label="getDealCurrentStepLabel"
:on-people-list-mode-change="(mode) => { peopleListMode = mode; }"
:on-people-search-input="(value) => { peopleSearch = value; }"
:on-people-sort-mode-change="(mode) => { peopleSortMode = mode; }"
/>
<div class="hidden h-12 items-center justify-between gap-2 border-b border-base-300 px-3 md:flex md:col-span-2">
<div v-if="selectedWorkspaceContact">
@@ -5830,146 +5514,32 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</article>
<aside class="h-full min-h-0">
<div class="flex h-full min-h-0 flex-col p-3">
<div
v-if="selectedWorkspaceContactDocuments.length"
class="mb-2 flex flex-wrap items-center gap-1.5 rounded-xl border border-base-300 bg-base-200/35 px-2 py-1.5"
>
<button
class="badge badge-sm badge-outline"
@click="contactRightPanelMode = 'documents'"
>
{{ selectedWorkspaceContactDocuments.length }} documents
</button>
<button
v-for="doc in selectedWorkspaceContactDocuments.slice(0, 15)"
:key="`contact-doc-chip-${doc.id}`"
class="rounded-full border border-base-300 bg-base-100 px-2 py-0.5 text-[10px] text-base-content/80 hover:bg-base-200/70"
@click="contactRightPanelMode = 'documents'; selectedDocumentId = doc.id"
>
{{ doc.title }}
</button>
</div>
<div v-if="contactRightPanelMode === 'documents'" class="min-h-0 flex-1 overflow-y-auto pr-1">
<div class="sticky top-0 z-10 border-b border-base-300 bg-base-100 pb-2">
<div class="flex items-center justify-between gap-2">
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
Contact documents
</p>
<button class="btn btn-ghost btn-xs" @click="contactRightPanelMode = 'summary'">Summary</button>
</div>
<input
v-model="contactDocumentsSearch"
type="text"
class="input input-bordered input-xs mt-2 w-full"
placeholder="Search documents..."
>
</div>
<div class="mt-2 space-y-1.5">
<article
v-for="doc in filteredSelectedWorkspaceContactDocuments"
:key="`contact-doc-right-${doc.id}`"
class="w-full rounded-xl border border-base-300 px-2.5 py-2 text-left transition hover:bg-base-200/50"
:class="selectedDocumentId === doc.id ? 'border-primary bg-primary/10' : ''"
@click="selectedDocumentId = doc.id"
>
<div class="flex items-start justify-between gap-2">
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
</div>
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
<div class="mt-1 flex items-center justify-between gap-2">
<p class="text-[10px] text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
<button class="btn btn-ghost btn-xs px-1" @click.stop="selectedDocumentId = doc.id; openDocumentsTab(true)">Open</button>
</div>
</article>
<p v-if="filteredSelectedWorkspaceContactDocuments.length === 0" class="px-1 py-2 text-xs text-base-content/55">
No linked documents.
</p>
</div>
</div>
<div v-else class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
<div
v-if="selectedWorkspaceDeal"
class="rounded-xl border border-base-300 bg-base-200/30 p-2.5"
:class="[
isReviewHighlightedDeal(selectedWorkspaceDeal.id) ? 'border-primary/60 bg-primary/10' : '',
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
hasContextScope('deal') ? 'context-scope-block-selected' : '',
]"
@click="toggleContextScope('deal')"
>
<span v-if="contextPickerEnabled" class="context-scope-label">Сделка</span>
<p class="text-sm font-medium">
{{ formatDealHeadline(selectedWorkspaceDeal) }}
</p>
<p class="mt-1 text-[11px] text-base-content/75">
{{ selectedWorkspaceDealSubtitle }}
</p>
<button
v-if="selectedWorkspaceDealSteps.length"
class="mt-2 text-[11px] font-medium text-primary hover:underline"
@click="selectedDealStepsExpanded = !selectedDealStepsExpanded"
>
{{ selectedDealStepsExpanded ? "Скрыть шаги" : `Показать шаги (${selectedWorkspaceDealSteps.length})` }}
</button>
<div v-if="selectedDealStepsExpanded && selectedWorkspaceDealSteps.length" class="mt-2 space-y-1.5">
<div
v-for="step in selectedWorkspaceDealSteps"
:key="step.id"
class="flex items-start gap-2 rounded-lg border border-base-300/70 bg-base-100/80 px-2 py-1.5"
>
<input
type="checkbox"
class="checkbox checkbox-xs mt-0.5"
:checked="isDealStepDone(step)"
disabled
>
<div class="min-w-0 flex-1">
<p class="truncate text-[11px] font-medium" :class="isDealStepDone(step) ? 'line-through text-base-content/60' : 'text-base-content/90'">
{{ step.title }}
</p>
<p class="mt-0.5 text-[10px] text-base-content/55">{{ formatDealStepMeta(step) }}</p>
</div>
</div>
</div>
</div>
<div
class="relative"
:class="[
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
hasContextScope('summary') ? 'context-scope-block-selected' : '',
]"
@click="toggleContextScope('summary')"
>
<span v-if="contextPickerEnabled" class="context-scope-label">Summary</span>
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-base-content/60">Summary</p>
<div
v-if="activeReviewContactDiff && selectedWorkspaceContact && activeReviewContactDiff.contactId === selectedWorkspaceContact.id"
class="mb-2 rounded-xl border border-primary/35 bg-primary/5 p-2"
>
<p class="text-[11px] font-semibold uppercase tracking-wide text-primary/80">Review diff</p>
<p class="mt-1 text-[11px] text-base-content/65">Before</p>
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-base-300/70 bg-base-100 px-2 py-1.5 text-[11px] leading-relaxed text-base-content/65 line-through">{{ activeReviewContactDiff.before || "Empty" }}</pre>
<p class="mt-2 text-[11px] text-base-content/65">After</p>
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-success/40 bg-success/10 px-2 py-1.5 text-[11px] leading-relaxed text-base-content">{{ activeReviewContactDiff.after || "Empty" }}</pre>
</div>
<ContactCollaborativeEditor
v-if="selectedWorkspaceContact"
:key="`contact-summary-${selectedWorkspaceContact.id}`"
v-model="selectedWorkspaceContact.description"
:room="`crm-contact-${selectedWorkspaceContact.id}`"
placeholder="Contact summary..."
:plain="true"
/>
<p v-else class="text-xs text-base-content/60">No contact selected.</p>
</div>
</div>
</div>
</aside>
<CrmCommunicationsContextSidebar
:selected-workspace-contact-documents="selectedWorkspaceContactDocuments"
:contact-right-panel-mode="contactRightPanelMode"
:on-contact-right-panel-mode-change="(mode) => { contactRightPanelMode = mode; }"
:selected-document-id="selectedDocumentId"
:on-selected-document-id-change="(documentId) => { selectedDocumentId = documentId; }"
:contact-documents-search="contactDocumentsSearch"
:on-contact-documents-search-input="(value) => { contactDocumentsSearch = value; }"
:filtered-selected-workspace-contact-documents="filteredSelectedWorkspaceContactDocuments"
:format-stamp="formatStamp"
:open-documents-tab="openDocumentsTab"
:selected-workspace-deal="selectedWorkspaceDeal"
:is-review-highlighted-deal="isReviewHighlightedDeal"
:context-picker-enabled="contextPickerEnabled"
:has-context-scope="hasContextScope"
:toggle-context-scope="toggleContextScope"
:format-deal-headline="formatDealHeadline"
:selected-workspace-deal-subtitle="selectedWorkspaceDealSubtitle"
:selected-workspace-deal-steps="selectedWorkspaceDealSteps"
:selected-deal-steps-expanded="selectedDealStepsExpanded"
:on-selected-deal-steps-expanded-change="(value) => { selectedDealStepsExpanded = value; }"
:is-deal-step-done="isDealStepDone"
:format-deal-step-meta="formatDealStepMeta"
:active-review-contact-diff="activeReviewContactDiff"
:selected-workspace-contact="selectedWorkspaceContact"
/>
</div>
</section>

View File

@@ -0,0 +1,222 @@
<script setup lang="ts">
type ContactRightPanelMode = "summary" | "documents";
defineProps<{
selectedWorkspaceContactDocuments: any[];
contactRightPanelMode: ContactRightPanelMode;
onContactRightPanelModeChange: (mode: ContactRightPanelMode) => void;
selectedDocumentId: string;
onSelectedDocumentIdChange: (documentId: string) => void;
contactDocumentsSearch: string;
onContactDocumentsSearchInput: (value: string) => void;
filteredSelectedWorkspaceContactDocuments: any[];
formatStamp: (iso: string) => string;
openDocumentsTab: (focusDocument?: boolean) => void;
selectedWorkspaceDeal: any | null;
isReviewHighlightedDeal: (dealId: string) => boolean;
contextPickerEnabled: boolean;
hasContextScope: (scope: "deal" | "summary") => boolean;
toggleContextScope: (scope: "deal" | "summary") => void;
formatDealHeadline: (deal: any) => string;
selectedWorkspaceDealSubtitle: string;
selectedWorkspaceDealSteps: any[];
selectedDealStepsExpanded: boolean;
onSelectedDealStepsExpandedChange: (value: boolean) => void;
isDealStepDone: (step: any) => boolean;
formatDealStepMeta: (step: any) => string;
activeReviewContactDiff: {
contactId?: string;
before?: string;
after?: string;
} | null;
selectedWorkspaceContact: {
id: string;
description: string;
} | null;
}>();
function onDocumentsSearchInput(event: Event) {
const target = event.target as HTMLInputElement | null;
onContactDocumentsSearchInput(target?.value ?? "");
}
</script>
<template>
<aside class="h-full min-h-0">
<div class="flex h-full min-h-0 flex-col p-3">
<div
v-if="selectedWorkspaceContactDocuments.length"
class="mb-2 flex flex-wrap items-center gap-1.5 rounded-xl border border-base-300 bg-base-200/35 px-2 py-1.5"
>
<button
class="badge badge-sm badge-outline"
@click="onContactRightPanelModeChange('documents')"
>
{{ selectedWorkspaceContactDocuments.length }} documents
</button>
<button
v-for="doc in selectedWorkspaceContactDocuments.slice(0, 15)"
:key="`contact-doc-chip-${doc.id}`"
class="rounded-full border border-base-300 bg-base-100 px-2 py-0.5 text-[10px] text-base-content/80 hover:bg-base-200/70"
@click="onContactRightPanelModeChange('documents'); onSelectedDocumentIdChange(doc.id)"
>
{{ doc.title }}
</button>
</div>
<div v-if="contactRightPanelMode === 'documents'" class="min-h-0 flex-1 overflow-y-auto pr-1">
<div class="sticky top-0 z-10 border-b border-base-300 bg-base-100 pb-2">
<div class="flex items-center justify-between gap-2">
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
Contact documents
</p>
<button class="btn btn-ghost btn-xs" @click="onContactRightPanelModeChange('summary')">Summary</button>
</div>
<input
:value="contactDocumentsSearch"
type="text"
class="input input-bordered input-xs mt-2 w-full"
placeholder="Search documents..."
@input="onDocumentsSearchInput"
>
</div>
<div class="mt-2 space-y-1.5">
<article
v-for="doc in filteredSelectedWorkspaceContactDocuments"
:key="`contact-doc-right-${doc.id}`"
class="w-full rounded-xl border border-base-300 px-2.5 py-2 text-left transition hover:bg-base-200/50"
:class="selectedDocumentId === doc.id ? 'border-primary bg-primary/10' : ''"
@click="onSelectedDocumentIdChange(doc.id)"
>
<div class="flex items-start justify-between gap-2">
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
</div>
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
<div class="mt-1 flex items-center justify-between gap-2">
<p class="text-[10px] text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
<button class="btn btn-ghost btn-xs px-1" @click.stop="onSelectedDocumentIdChange(doc.id); openDocumentsTab(true)">Open</button>
</div>
</article>
<p v-if="filteredSelectedWorkspaceContactDocuments.length === 0" class="px-1 py-2 text-xs text-base-content/55">
No linked documents.
</p>
</div>
</div>
<div v-else class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
<div
v-if="selectedWorkspaceDeal"
class="rounded-xl border border-base-300 bg-base-200/30 p-2.5"
:class="[
isReviewHighlightedDeal(selectedWorkspaceDeal.id) ? 'border-primary/60 bg-primary/10' : '',
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
hasContextScope('deal') ? 'context-scope-block-selected' : '',
]"
@click="toggleContextScope('deal')"
>
<span v-if="contextPickerEnabled" class="context-scope-label">Сделка</span>
<p class="text-sm font-medium">
{{ formatDealHeadline(selectedWorkspaceDeal) }}
</p>
<p class="mt-1 text-[11px] text-base-content/75">
{{ selectedWorkspaceDealSubtitle }}
</p>
<button
v-if="selectedWorkspaceDealSteps.length"
class="mt-2 text-[11px] font-medium text-primary hover:underline"
@click="onSelectedDealStepsExpandedChange(!selectedDealStepsExpanded)"
>
{{ selectedDealStepsExpanded ? "Скрыть шаги" : `Показать шаги (${selectedWorkspaceDealSteps.length})` }}
</button>
<div v-if="selectedDealStepsExpanded && selectedWorkspaceDealSteps.length" class="mt-2 space-y-1.5">
<div
v-for="step in selectedWorkspaceDealSteps"
:key="step.id"
class="flex items-start gap-2 rounded-lg border border-base-300/70 bg-base-100/80 px-2 py-1.5"
>
<input
type="checkbox"
class="checkbox checkbox-xs mt-0.5"
:checked="isDealStepDone(step)"
disabled
>
<div class="min-w-0 flex-1">
<p class="truncate text-[11px] font-medium" :class="isDealStepDone(step) ? 'line-through text-base-content/60' : 'text-base-content/90'">
{{ step.title }}
</p>
<p class="mt-0.5 text-[10px] text-base-content/55">{{ formatDealStepMeta(step) }}</p>
</div>
</div>
</div>
</div>
<div
class="relative"
:class="[
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
hasContextScope('summary') ? 'context-scope-block-selected' : '',
]"
@click="toggleContextScope('summary')"
>
<span v-if="contextPickerEnabled" class="context-scope-label">Summary</span>
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-base-content/60">Summary</p>
<div
v-if="activeReviewContactDiff && selectedWorkspaceContact && activeReviewContactDiff.contactId === selectedWorkspaceContact.id"
class="mb-2 rounded-xl border border-primary/35 bg-primary/5 p-2"
>
<p class="text-[11px] font-semibold uppercase tracking-wide text-primary/80">Review diff</p>
<p class="mt-1 text-[11px] text-base-content/65">Before</p>
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-base-300/70 bg-base-100 px-2 py-1.5 text-[11px] leading-relaxed text-base-content/65 line-through">{{ activeReviewContactDiff.before || "Empty" }}</pre>
<p class="mt-2 text-[11px] text-base-content/65">After</p>
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-success/40 bg-success/10 px-2 py-1.5 text-[11px] leading-relaxed text-base-content">{{ activeReviewContactDiff.after || "Empty" }}</pre>
</div>
<ContactCollaborativeEditor
v-if="selectedWorkspaceContact"
:key="`contact-summary-${selectedWorkspaceContact.id}`"
v-model="selectedWorkspaceContact.description"
:room="`crm-contact-${selectedWorkspaceContact.id}`"
placeholder="Contact summary..."
:plain="true"
/>
<p v-else class="text-xs text-base-content/60">No contact selected.</p>
</div>
</div>
</div>
</aside>
</template>
<style scoped>
.context-scope-block {
position: relative;
border-radius: 16px;
outline: 1px solid color-mix(in oklab, var(--color-base-content) 14%, transparent);
transition: outline-color 160ms ease, box-shadow 160ms ease;
}
.context-scope-block-active {
outline-color: color-mix(in oklab, var(--color-primary) 52%, transparent);
box-shadow:
0 0 0 1px color-mix(in oklab, var(--color-primary) 30%, transparent) inset,
0 0 0 3px color-mix(in oklab, var(--color-primary) 12%, transparent);
}
.context-scope-block-selected {
outline-color: color-mix(in oklab, var(--color-primary) 70%, transparent);
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 40%, transparent) inset;
}
.context-scope-label {
position: absolute;
top: -10px;
left: 12px;
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
background: color-mix(in oklab, var(--color-primary) 18%, var(--color-base-100));
color: color-mix(in oklab, var(--color-primary-content) 72%, var(--color-base-content));
border: 1px solid color-mix(in oklab, var(--color-primary) 42%, transparent);
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,206 @@
<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;
threadChannelLabel: (thread: any) => 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-start justify-between gap-2">
<p class="truncate text-xs font-semibold">{{ thread.contact }}</p>
<span class="shrink-0 text-[10px] text-base-content/55">{{ formatThreadTime(thread.lastAt) }}</span>
</div>
<div class="mt-0.5 flex items-center justify-between gap-2">
<p class="min-w-0 flex-1 truncate text-[11px] text-base-content/75">{{ threadChannelLabel(thread) }}</p>
<div class="dropdown dropdown-end" @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>