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 "";
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3500,8 +3542,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
class="min-h-0 flex-1"
|
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'"
|
: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">
|
<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="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button class="btn btn-xs" @click="setToday">Today</button>
|
<button class="btn btn-xs" @click="setToday">Today</button>
|
||||||
<button class="btn btn-xs btn-ghost" @click="shiftCalendar(-1)">←</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.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto pr-1">
|
<article
|
||||||
<div v-if="calendarView === 'month'" class="space-y-1">
|
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">
|
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
|
||||||
<span>Sun</span>
|
<span>Sun</span>
|
||||||
<span>Mon</span>
|
<span>Mon</span>
|
||||||
@@ -3542,18 +3596,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
v-for="cell in monthCells"
|
v-for="cell in monthCells"
|
||||||
:key="cell.key"
|
:key="cell.key"
|
||||||
class="min-h-24 rounded-lg border p-1 text-left"
|
class="min-h-24 rounded-lg border p-1 text-left"
|
||||||
: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>
|
>
|
||||||
<button
|
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
||||||
v-for="event in cell.events.slice(0, 2)"
|
<button
|
||||||
:key="event.id"
|
v-for="event in monthCellEvents(cell.events)"
|
||||||
class="block w-full truncate rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline"
|
:key="event.id"
|
||||||
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''"
|
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)"
|
@click.stop="openThreadFromCalendarItem(event)"
|
||||||
>
|
>
|
||||||
{{ formatTime(event.start) }} {{ event.title }}
|
{{ formatTime(event.start) }} {{ event.title }}
|
||||||
@@ -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>
|
||||||
@@ -4249,13 +4307,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
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
|
||||||
<p class="text-sm">{{ entry.item.text }}</p>
|
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">
|
<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">
|
||||||
<svg v-if="channelIcon(entry.item.channel) === 'telegram'" viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
|
<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