From aa465f65bdfb0f29e80404e1773b40d8dbefb155 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:38:30 +0700 Subject: [PATCH] feat(workspace): add hidden contacts filter and remove calendar scene swap --- .../components/workspace/CrmWorkspaceApp.vue | 125 +++++++++++++----- .../workspace/calendar/CrmCalendarPanel.vue | 6 - .../CrmCommunicationsListSidebar.vue | 16 ++- 3 files changed, 107 insertions(+), 40 deletions(-) diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index f1d4ddd..8a03702 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -41,6 +41,7 @@ type CalendarView = "day" | "week" | "month" | "year" | "agenda"; type SortMode = "name" | "lastContact"; type PeopleLeftMode = "contacts" | "calendar"; type PeopleSortMode = "name" | "lastContact" | "company" | "country"; +type PeopleVisibilityMode = "all" | "hidden"; type DocumentSortMode = "updatedAt" | "title" | "owner"; type FeedCard = { @@ -2397,7 +2398,6 @@ const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({ }); const calendarZoomGhost = ref(null); const calendarZoomBusy = ref(false); -const calendarSceneMasked = ref(false); const calendarCameraState = ref({ active: false, left: 0, @@ -2412,6 +2412,8 @@ let calendarWheelLockUntil = 0; let calendarZoomPrimeTimer: ReturnType | null = null; let calendarZoomPrimeLastAt = 0; const CALENDAR_ZOOM_DURATION_MS = 2400; +const CALENDAR_ZOOM_FOCUS_MS = 1400; +const CALENDAR_ZOOM_REVEAL_MS = Math.max(500, CALENDAR_ZOOM_DURATION_MS - CALENDAR_ZOOM_FOCUS_MS); const CALENDAR_ZOOM_PRIME_STEPS = 2; const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05; const CALENDAR_ZOOM_PRIME_RESET_MS = 900; @@ -2762,11 +2764,15 @@ function waitCalendarZoomTransition() { }); } -async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: CalendarZoomGhost, apply: () => void) { +async function animateCalendarZoomIn( + sourceElement: HTMLElement | null, + ghost: CalendarZoomGhost, + apply: () => void, + resolveRevealTarget?: () => HTMLElement | null, +) { clearCalendarZoomPrime(); calendarZoomBusy.value = true; clearCalendarZoomOverlay(); - calendarSceneMasked.value = false; try { calendarZoomGhost.value = ghost; const fromRect = getElementRectInScene(sourceElement) ?? fallbackZoomOriginRectInScene(); @@ -2790,19 +2796,36 @@ async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: C left: cameraTarget.left, top: cameraTarget.top, scale: cameraTarget.scale, - durationMs: CALENDAR_ZOOM_DURATION_MS, + durationMs: CALENDAR_ZOOM_FOCUS_MS, }; await waitCalendarCameraTransition(); - // Freeze the filled block frame, then swap level while scene is masked. - // This keeps the "zoom into block -> reveal next grid inside" sequence. - calendarSceneMasked.value = true; - await nextAnimationFrame(); apply(); await nextTick(); + const revealTargetElement = resolveRevealTarget ? resolveRevealTarget() : sourceElement; + const revealTargetRect = getElementRectInScene(revealTargetElement) ?? fallbackZoomOriginRectInScene(); + const revealTarget = revealTargetRect ? cameraTransformForRect(revealTargetRect) : null; + if (revealTarget) { + calendarCameraState.value = { + active: true, + left: revealTarget.left, + top: revealTarget.top, + scale: revealTarget.scale, + durationMs: 0, + }; + await nextTick(); + await nextAnimationFrame(); + } + calendarCameraState.value = { + active: true, + left: 0, + top: 0, + scale: 1, + durationMs: CALENDAR_ZOOM_REVEAL_MS, + }; + await waitCalendarCameraTransition(); } finally { await resetCalendarCamera(); calendarZoomGhost.value = null; - calendarSceneMasked.value = false; calendarZoomBusy.value = false; } } @@ -2813,15 +2836,11 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT clearCalendarZoomOverlay(); try { calendarZoomGhost.value = zoomGhostForCurrentView(); - calendarSceneMasked.value = true; apply(); await nextTick(); const targetRect = getElementRectInScene(resolveTarget()) ?? fallbackZoomOriginRectInScene(); const cameraStart = targetRect ? cameraTransformForRect(targetRect) : null; - if (!cameraStart) { - calendarSceneMasked.value = false; - return; - } + if (!cameraStart) return; calendarCameraState.value = { active: true, left: cameraStart.left, @@ -2832,8 +2851,6 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT await nextTick(); await nextAnimationFrame(); calendarSceneRef.value?.getBoundingClientRect(); - calendarSceneMasked.value = false; - await nextAnimationFrame(); calendarCameraState.value = { active: true, left: 0, @@ -2845,7 +2862,6 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT } finally { await resetCalendarCamera(); calendarZoomGhost.value = null; - calendarSceneMasked.value = false; calendarZoomBusy.value = false; } } @@ -2895,7 +2911,7 @@ async function zoomInCalendar(event?: Event) { if (maybePrimeWheelZoom(wheelEvent, calendarPrimeMonthToken(monthIndex))) return; await animateCalendarZoomIn(sourceElement, zoomGhostForMonth(monthIndex), () => { openYearMonth(monthIndex); - }); + }, () => queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`)); return; } @@ -2914,6 +2930,9 @@ async function zoomInCalendar(event?: Event) { () => { openWeekView(anchorDayKey); }, + () => + queryCalendarElement(`[data-calendar-week-start-key="${rowStartKey}"]`) ?? + queryCalendarElement(`[data-calendar-day-key="${anchorDayKey}"]`), ); return; } @@ -2924,7 +2943,7 @@ async function zoomInCalendar(event?: Event) { if (maybePrimeWheelZoom(wheelEvent, calendarPrimeDayToken(dayAnchor))) return; await animateCalendarZoomIn(sourceElement, zoomGhostForDay(dayAnchor), () => { openDayView(dayAnchor); - }); + }, () => queryCalendarElement(`[data-calendar-day-key="${dayAnchor}"]`)); } } @@ -2935,6 +2954,7 @@ async function zoomToMonth(monthIndex: number) { () => { openYearMonth(monthIndex); }, + () => queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), ); } @@ -3366,6 +3386,7 @@ function openDocumentsTab(push = false) { const peopleListMode = ref<"contacts" | "deals">("contacts"); const peopleSearch = ref(""); const peopleSortMode = ref("lastContact"); +const peopleVisibilityMode = ref("all"); const brokenAvatarByContactId = ref>({}); const peopleSortOptions: Array<{ value: PeopleSortMode; label: string }> = [ { value: "lastContact", label: "Last contact" }, @@ -3373,6 +3394,10 @@ const peopleSortOptions: Array<{ value: PeopleSortMode; label: string }> = [ { value: "company", label: "Company" }, { value: "country", label: "Country" }, ]; +const peopleVisibilityOptions: Array<{ value: PeopleVisibilityMode; label: string }> = [ + { value: "all", label: "All" }, + { value: "hidden", label: "Hidden" }, +]; const selectedDealId = ref(deals.value[0]?.id ?? ""); const selectedDealStepsExpanded = ref(false); @@ -3412,26 +3437,54 @@ const commThreads = computed(() => { map.get(item.contact)?.push(item); } - return contacts.value - .map((contact) => { - const items = map.get(contact.name) ?? []; + const contactById = new Map(contacts.value.map((contact) => [contact.id, contact])); + const inboxesByContactId = new Map(); + for (const inbox of contactInboxes.value) { + if (!inboxesByContactId.has(inbox.contactId)) { + inboxesByContactId.set(inbox.contactId, []); + } + inboxesByContactId.get(inbox.contactId)?.push(inbox); + } + + const contactIds = new Set([ + ...contacts.value.map((contact) => contact.id), + ...contactInboxes.value.map((inbox) => inbox.contactId), + ]); + + return [...contactIds] + .map((contactId) => { + const contact = contactById.get(contactId); + const inboxes = inboxesByContactId.get(contactId) ?? []; + const contactName = contact?.name ?? inboxes[0]?.contactName ?? ""; + const items = map.get(contactName) ?? []; const last = items[items.length - 1]; - const channels = [...new Set([...contact.channels, ...items.map((item) => item.channel)])] as CommItem["channel"][]; + const channels = [ + ...new Set([ + ...(contact?.channels ?? []), + ...inboxes.map((inbox) => inbox.channel), + ...items.map((item) => item.channel), + ]), + ] as CommItem["channel"][]; + const inboxFallbackLast = inboxes + .map((inbox) => inbox.lastMessageAt || inbox.updatedAt) + .filter(Boolean) + .sort() + .at(-1); return { - id: contact.id, - contact: contact.name, - avatar: contact.avatar, - company: contact.company, - country: contact.country, - location: contact.location, + id: contactId, + contact: contactName, + avatar: contact?.avatar ?? "", + company: contact?.company ?? "", + country: contact?.country ?? "", + location: contact?.location ?? "", channels, - lastAt: last?.at ?? contact.lastContactAt, + lastAt: last?.at ?? contact?.lastContactAt ?? inboxFallbackLast ?? "", lastText: last?.text ?? "No messages yet", items, }; }) - .filter((thread) => thread.items.length > 0) + .filter((thread) => thread.contact) .sort((a, b) => b.lastAt.localeCompare(a.lastAt)); }); @@ -3442,8 +3495,12 @@ const peopleContactList = computed(() => { const haystack = [item.contact, item.company, item.country, item.location].join(" ").toLowerCase(); return haystack.includes(query); }); + const byVisibility = list.filter((item) => { + if (peopleVisibilityMode.value === "all") return true; + return threadInboxes(item).some((inbox) => inbox.isHidden); + }); - return list.sort((a, b) => { + return byVisibility.sort((a, b) => { if (peopleSortMode.value === "name") return a.contact.localeCompare(b.contact); if (peopleSortMode.value === "company") return a.company.localeCompare(b.company); if (peopleSortMode.value === "country") return a.country.localeCompare(b.country); @@ -4853,7 +4910,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") :on-calendar-hierarchy-wheel="onCalendarHierarchyWheel" :set-calendar-scene-ref="setCalendarSceneRef" :normalized-calendar-view="normalizedCalendarView" - :calendar-scene-masked="calendarSceneMasked" :calendar-scene-transform-style="calendarSceneTransformStyle" :on-calendar-scene-mouse-leave="onCalendarSceneMouseLeave" :calendar-view="calendarView" @@ -4894,6 +4950,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") :people-search="peopleSearch" :people-sort-options="peopleSortOptions" :people-sort-mode="peopleSortMode" + :people-visibility-options="peopleVisibilityOptions" + :people-visibility-mode="peopleVisibilityMode" :people-contact-list="peopleContactList" :selected-comm-thread-id="selectedCommThreadId" :is-review-highlighted-contact="isReviewHighlightedContact" @@ -4915,6 +4973,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") :on-people-list-mode-change="(mode) => { peopleListMode = mode; }" :on-people-search-input="(value) => { peopleSearch = value; }" :on-people-sort-mode-change="(mode) => { peopleSortMode = mode; }" + :on-people-visibility-mode-change="(mode) => { peopleVisibilityMode = mode; }" />