Fix live review navigation and in-UI highlight behavior

This commit is contained in:
Ruslan Bakiev
2026-02-21 10:08:22 +07:00
parent e2e2901076
commit edea7a0034

View File

@@ -1267,6 +1267,11 @@ const activeReviewDealId = computed(() => {
if (!item || item.entity !== "deal" || !item.entityId) return ""; if (!item || item.entity !== "deal" || !item.entityId) return "";
return item.entityId; return item.entityId;
}); });
const activeReviewMessageId = computed(() => {
const item = activeChangeItem.value;
if (!item || item.entity !== "message" || !item.entityId) return "";
return item.entityId;
});
const activeReviewContactDiff = computed(() => { const activeReviewContactDiff = computed(() => {
const item = activeChangeItem.value; const item = activeChangeItem.value;
if (!item || item.entity !== "contact_note" || !item.entityId) return null; if (!item || item.entity !== "contact_note" || !item.entityId) return null;
@@ -1310,6 +1315,7 @@ function normalizeChangeText(raw: string | null | undefined) {
function describeChangeEntity(entity: string) { function describeChangeEntity(entity: string) {
if (entity === "contact_note") return "Contact summary"; if (entity === "contact_note") return "Contact summary";
if (entity === "calendar_event") return "Calendar event"; if (entity === "calendar_event") return "Calendar event";
if (entity === "message") return "Message";
if (entity === "deal") return "Deal"; if (entity === "deal") return "Deal";
return entity || "Change"; return entity || "Change";
} }
@@ -1680,6 +1686,10 @@ function isReviewHighlightedDeal(dealId: string) {
return Boolean(reviewActive.value && activeReviewDealId.value && activeReviewDealId.value === dealId); return Boolean(reviewActive.value && activeReviewDealId.value && activeReviewDealId.value === dealId);
} }
function isReviewHighlightedMessage(messageId: string) {
return Boolean(reviewActive.value && activeReviewMessageId.value && activeReviewMessageId.value === messageId);
}
function applyReviewStepToUi(push = false) { function applyReviewStepToUi(push = false) {
const item = activeChangeItem.value; const item = activeChangeItem.value;
if (!item) { if (!item) {
@@ -1729,6 +1739,18 @@ function applyReviewStepToUi(push = false) {
return; return;
} }
if (item.entity === "message" && item.entityId) {
peopleLeftMode.value = "contacts";
peopleListMode.value = "contacts";
const message = commItems.value.find((entry) => entry.id === item.entityId);
if (message?.contact) {
openCommunicationThread(message.contact);
}
focusedCalendarEventId.value = "";
syncPathFromUi(push);
return;
}
peopleLeftMode.value = "contacts"; peopleLeftMode.value = "contacts";
focusedCalendarEventId.value = ""; focusedCalendarEventId.value = "";
syncPathFromUi(push); syncPathFromUi(push);
@@ -1825,6 +1847,11 @@ const calendarCursor = ref(new Date(new Date().getFullYear(), new Date().getMont
const selectedDateKey = ref(dayKey(new Date())); const selectedDateKey = ref(dayKey(new Date()));
const sortedEvents = computed(() => [...calendarEvents.value].sort((a, b) => a.start.localeCompare(b.start))); const sortedEvents = computed(() => [...calendarEvents.value].sort((a, b) => a.start.localeCompare(b.start)));
const focusedCalendarEvent = computed(() => {
const id = focusedCalendarEventId.value.trim();
if (!id) return null;
return sortedEvents.value.find((event) => event.id === id) ?? null;
});
const eventsByDate = computed(() => { const eventsByDate = computed(() => {
const map = new Map<string, CalendarEvent[]>(); const map = new Map<string, CalendarEvent[]>();
@@ -1876,6 +1903,21 @@ const monthCells = computed(() => {
}); });
}); });
function monthCellHasFocusedEvent(events: CalendarEvent[]) {
const id = focusedCalendarEventId.value.trim();
if (!id) return false;
return events.some((event) => event.id === id);
}
function monthCellEvents(events: CalendarEvent[]) {
const id = focusedCalendarEventId.value.trim();
if (!id) return events.slice(0, 2);
const focused = events.find((event) => event.id === id);
if (!focused) return events.slice(0, 2);
const rest = events.filter((event) => event.id !== id).slice(0, 1);
return [focused, ...rest];
}
const weekDays = computed(() => { const weekDays = computed(() => {
const base = new Date(`${selectedDateKey.value}T00:00:00`); const base = new Date(`${selectedDateKey.value}T00:00:00`);
const mondayOffset = (base.getDay() + 6) % 7; const mondayOffset = (base.getDay() + 6) % 7;
@@ -2623,6 +2665,8 @@ async function togglePinForEntry(entry: any) {
} }
const selectedWorkspaceContact = computed(() => { const selectedWorkspaceContact = computed(() => {
if (selectedContact.value) return selectedContact.value;
const threadContactId = (selectedCommThread.value?.id ?? "").trim(); const threadContactId = (selectedCommThread.value?.id ?? "").trim();
if (threadContactId) { if (threadContactId) {
const byId = contacts.value.find((contact) => contact.id === threadContactId); const byId = contacts.value.find((contact) => contact.id === threadContactId);
@@ -2634,8 +2678,6 @@ const selectedWorkspaceContact = computed(() => {
const byName = contacts.value.find((contact) => contact.name === threadContactName); const byName = contacts.value.find((contact) => contact.name === threadContactName);
if (byName) return byName; if (byName) return byName;
} }
if (selectedContact.value) return selectedContact.value;
return contacts.value[0] ?? null; return contacts.value[0] ?? null;
}); });
@@ -3525,6 +3567,18 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div> </div>
</div> </div>
<article
v-if="focusedCalendarEvent"
class="rounded-xl border border-success/50 bg-success/10 px-3 py-2"
>
<p class="text-xs font-semibold uppercase tracking-wide text-success/80">Review focus event</p>
<p class="text-sm font-medium text-base-content">{{ focusedCalendarEvent.title }}</p>
<p class="text-xs text-base-content/70">
{{ formatDay(focusedCalendarEvent.start) }} · {{ formatTime(focusedCalendarEvent.start) }} - {{ formatTime(focusedCalendarEvent.end) }}
</p>
<p class="mt-1 text-xs text-base-content/80">{{ focusedCalendarEvent.note || "No note" }}</p>
</article>
<div class="min-h-0 flex-1 overflow-y-auto pr-1"> <div class="min-h-0 flex-1 overflow-y-auto pr-1">
<div v-if="calendarView === 'month'" class="space-y-1"> <div v-if="calendarView === 'month'" class="space-y-1">
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60"> <div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
@@ -3545,12 +3599,13 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
:class="[ :class="[
cell.inMonth ? 'border-base-300 bg-base-100' : 'border-base-200 bg-base-200/40 text-base-content/40', cell.inMonth ? 'border-base-300 bg-base-100' : 'border-base-200 bg-base-200/40 text-base-content/40',
selectedDateKey === cell.key ? 'border-primary bg-primary/5' : '', selectedDateKey === cell.key ? 'border-primary bg-primary/5' : '',
monthCellHasFocusedEvent(cell.events) ? 'border-success/60 bg-success/10' : '',
]" ]"
@click="pickDate(cell.key)" @click="pickDate(cell.key)"
> >
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p> <p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
<button <button
v-for="event in cell.events.slice(0, 2)" v-for="event in monthCellEvents(cell.events)"
:key="event.id" :key="event.id"
class="block w-full truncate rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline" class="block w-full truncate rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline"
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''" :class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''"
@@ -4119,7 +4174,10 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div> </div>
<div v-else-if="entry.kind === 'call'" class="flex justify-center"> <div v-else-if="entry.kind === 'call'" class="flex justify-center">
<div class="call-wave-card w-full max-w-[460px] rounded-2xl border border-base-300 px-4 py-3 text-center"> <div
class="call-wave-card w-full max-w-[460px] rounded-2xl border border-base-300 px-4 py-3 text-center"
:class="isReviewHighlightedMessage(entry.item.id) ? 'border-success/60 bg-success/10 ring-2 ring-success/40' : ''"
>
<p class="mb-2 text-xs text-base-content/65"> <p class="mb-2 text-xs text-base-content/65">
{{ formatDay(entry.item.at) }} · {{ formatTime(entry.item.at) }} {{ formatDay(entry.item.at) }} · {{ formatTime(entry.item.at) }}
<span v-if="entry.item.duration"> · {{ entry.item.duration }}</span> <span v-if="entry.item.duration"> · {{ entry.item.duration }}</span>
@@ -4254,7 +4312,13 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
class="flex" class="flex"
:class="entry.item.direction === 'out' ? 'justify-end' : 'justify-start'" :class="entry.item.direction === 'out' ? 'justify-end' : 'justify-start'"
> >
<div class="max-w-[88%] rounded-xl border border-base-300 p-3" :class="entry.item.direction === 'out' ? 'bg-base-200' : 'bg-base-100'"> <div
class="max-w-[88%] rounded-xl border border-base-300 p-3"
:class="[
entry.item.direction === 'out' ? 'bg-base-200' : 'bg-base-100',
isReviewHighlightedMessage(entry.item.id) ? 'border-success/60 bg-success/10 ring-2 ring-success/40' : '',
]"
>
<p class="text-sm">{{ entry.item.text }}</p> <p class="text-sm">{{ entry.item.text }}</p>
<p class="mt-1 text-xs text-base-content/60"> <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"> <span class="mr-1 inline-flex h-4 w-4 items-center justify-center align-middle">