Fix live review navigation and in-UI highlight behavior
This commit is contained in:
120
frontend/app.vue
120
frontend/app.vue
@@ -1267,6 +1267,11 @@ const activeReviewDealId = computed(() => {
|
||||
if (!item || item.entity !== "deal" || !item.entityId) return "";
|
||||
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 item = activeChangeItem.value;
|
||||
if (!item || item.entity !== "contact_note" || !item.entityId) return null;
|
||||
@@ -1310,6 +1315,7 @@ function normalizeChangeText(raw: string | null | undefined) {
|
||||
function describeChangeEntity(entity: string) {
|
||||
if (entity === "contact_note") return "Contact summary";
|
||||
if (entity === "calendar_event") return "Calendar event";
|
||||
if (entity === "message") return "Message";
|
||||
if (entity === "deal") return "Deal";
|
||||
return entity || "Change";
|
||||
}
|
||||
@@ -1680,6 +1686,10 @@ function isReviewHighlightedDeal(dealId: string) {
|
||||
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) {
|
||||
const item = activeChangeItem.value;
|
||||
if (!item) {
|
||||
@@ -1729,6 +1739,18 @@ function applyReviewStepToUi(push = false) {
|
||||
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";
|
||||
focusedCalendarEventId.value = "";
|
||||
syncPathFromUi(push);
|
||||
@@ -1825,6 +1847,11 @@ const calendarCursor = ref(new Date(new Date().getFullYear(), new Date().getMont
|
||||
const selectedDateKey = ref(dayKey(new Date()));
|
||||
|
||||
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 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 base = new Date(`${selectedDateKey.value}T00:00:00`);
|
||||
const mondayOffset = (base.getDay() + 6) % 7;
|
||||
@@ -2623,6 +2665,8 @@ async function togglePinForEntry(entry: any) {
|
||||
}
|
||||
|
||||
const selectedWorkspaceContact = computed(() => {
|
||||
if (selectedContact.value) return selectedContact.value;
|
||||
|
||||
const threadContactId = (selectedCommThread.value?.id ?? "").trim();
|
||||
if (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);
|
||||
if (byName) return byName;
|
||||
}
|
||||
|
||||
if (selectedContact.value) return selectedContact.value;
|
||||
return contacts.value[0] ?? null;
|
||||
});
|
||||
|
||||
@@ -3500,8 +3542,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
class="min-h-0 flex-1"
|
||||
:class="selectedTab === 'communications' && peopleLeftMode === 'contacts' ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
||||
>
|
||||
<section v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'" class="flex h-full min-h-0 flex-col gap-3">
|
||||
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||
<section v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'" class="flex h-full min-h-0 flex-col gap-3">
|
||||
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="btn btn-xs" @click="setToday">Today</button>
|
||||
<button class="btn btn-xs btn-ghost" @click="shiftCalendar(-1)">←</button>
|
||||
@@ -3522,11 +3564,23 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div v-if="calendarView === 'month'" class="space-y-1">
|
||||
<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 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">
|
||||
<span>Sun</span>
|
||||
<span>Mon</span>
|
||||
@@ -3542,18 +3596,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
v-for="cell in monthCells"
|
||||
:key="cell.key"
|
||||
class="min-h-24 rounded-lg border p-1 text-left"
|
||||
:class="[
|
||||
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' : '',
|
||||
]"
|
||||
@click="pickDate(cell.key)"
|
||||
>
|
||||
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
||||
<button
|
||||
v-for="event in cell.events.slice(0, 2)"
|
||||
:key="event.id"
|
||||
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="[
|
||||
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' : '',
|
||||
monthCellHasFocusedEvent(cell.events) ? 'border-success/60 bg-success/10' : '',
|
||||
]"
|
||||
@click="pickDate(cell.key)"
|
||||
>
|
||||
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
||||
<button
|
||||
v-for="event in monthCellEvents(cell.events)"
|
||||
:key="event.id"
|
||||
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' : ''"
|
||||
@click.stop="openThreadFromCalendarItem(event)"
|
||||
>
|
||||
{{ formatTime(event.start) }} {{ event.title }}
|
||||
@@ -4119,7 +4174,10 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ formatDay(entry.item.at) }} · {{ formatTime(entry.item.at) }}
|
||||
<span v-if="entry.item.duration"> · {{ entry.item.duration }}</span>
|
||||
@@ -4249,13 +4307,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex"
|
||||
: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'">
|
||||
<p class="text-sm">{{ entry.item.text }}</p>
|
||||
<div
|
||||
v-else
|
||||
class="flex"
|
||||
: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',
|
||||
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="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(entry.item.channel) === 'telegram'" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
||||
|
||||
Reference in New Issue
Block a user