223 lines
9.7 KiB
Vue
223 lines
9.7 KiB
Vue
<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>
|