diff --git a/frontend/app/pages/index.vue b/frontend/app/pages/index.vue index 1168ef7..1bf839e 100644 --- a/frontend/app/pages/index.vue +++ b/frontend/app/pages/index.vue @@ -17,6 +17,7 @@ import createChatConversationMutation from "~~/graphql/operations/create-chat-co import selectChatConversationMutation from "~~/graphql/operations/select-chat-conversation.graphql?raw"; import archiveChatConversationMutation from "~~/graphql/operations/archive-chat-conversation.graphql?raw"; import toggleContactPinMutation from "~~/graphql/operations/toggle-contact-pin.graphql?raw"; +import setContactInboxHiddenMutation from "~~/graphql/operations/set-contact-inbox-hidden.graphql?raw"; import confirmLatestChangeSetMutation from "~~/graphql/operations/confirm-latest-change-set.graphql?raw"; import rollbackLatestChangeSetMutation from "~~/graphql/operations/rollback-latest-change-set.graphql?raw"; import rollbackChangeSetItemsMutation from "~~/graphql/operations/rollback-change-set-items.graphql?raw"; @@ -79,6 +80,9 @@ type CommItem = { id: string; at: string; contact: string; + contactInboxId: string; + sourceExternalId: string; + sourceTitle: string; channel: "Telegram" | "WhatsApp" | "Instagram" | "Phone" | "Email"; kind: "message" | "call"; direction: "in" | "out"; @@ -89,6 +93,18 @@ type CommItem = { deliveryStatus?: "PENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED" | string | null; }; +type ContactInbox = { + id: string; + contactId: string; + contactName: string; + channel: CommItem["channel"]; + sourceExternalId: string; + title: string; + isHidden: boolean; + lastMessageAt: string; + updatedAt: string; +}; + type CommPin = { id: string; contact: string; @@ -302,6 +318,7 @@ const contacts = ref([]); const calendarEvents = ref([]); const commItems = ref([]); +const contactInboxes = ref([]); const commPins = ref([]); @@ -952,6 +969,7 @@ async function refreshCrmData() { dashboard: { contacts: Contact[]; communications: CommItem[]; + contactInboxes: ContactInbox[]; calendar: CalendarEvent[]; deals: Deal[]; feed: FeedCard[]; @@ -962,6 +980,7 @@ async function refreshCrmData() { contacts.value = data.dashboard.contacts ?? []; commItems.value = data.dashboard.communications ?? []; + contactInboxes.value = data.dashboard.contactInboxes ?? []; calendarEvents.value = data.dashboard.calendar ?? []; deals.value = data.dashboard.deals ?? []; feedCards.value = data.dashboard.feed ?? []; @@ -2696,11 +2715,16 @@ async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: C durationMs: CALENDAR_ZOOM_DURATION_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(); } finally { await resetCalendarCamera(); calendarZoomGhost.value = null; + calendarSceneMasked.value = false; calendarZoomBusy.value = false; } } @@ -3324,6 +3348,7 @@ const commThreads = computed(() => { items, }; }) + .filter((thread) => thread.items.length > 0) .sort((a, b) => b.lastAt.localeCompare(a.lastAt)); }); @@ -3426,6 +3451,7 @@ const commDocumentForm = ref<{ }>({ title: "", }); +const inboxToggleLoadingById = ref>({}); const eventCloseOpen = ref>({}); const eventCloseDraft = ref>({}); const eventCloseSaving = ref>({}); @@ -3453,6 +3479,7 @@ watch(selectedCommThreadId, () => { commPinContextMenu.value = { open: false, x: 0, y: 0, entry: null }; commEventError.value = ""; commDocumentForm.value = { title: "" }; + inboxToggleLoadingById.value = {}; eventCloseOpen.value = {}; eventCloseDraft.value = {}; eventCloseSaving.value = {}; @@ -3488,6 +3515,26 @@ const selectedCommPins = computed(() => { return commPins.value.filter((item) => item.contact === selectedCommThread.value?.contact); }); +const selectedThreadInboxes = computed(() => { + if (!selectedCommThread.value) return []; + const all = contactInboxes.value.filter((inbox) => inbox.contactId === selectedCommThread.value?.id); + return all.sort((a, b) => { + const aTime = a.lastMessageAt || a.updatedAt; + const bTime = b.lastMessageAt || b.updatedAt; + return bTime.localeCompare(aTime); + }); +}); + +const hiddenContactInboxes = computed(() => + contactInboxes.value + .filter((inbox) => inbox.isHidden) + .sort((a, b) => { + const aTime = a.lastMessageAt || a.updatedAt; + const bTime = b.lastMessageAt || b.updatedAt; + return bTime.localeCompare(aTime); + }), +); + const selectedCommLifecycleEvents = computed(() => { if (!selectedCommThread.value) return []; const nowMs = lifecycleNowMs.value; @@ -4215,6 +4262,34 @@ function channelIcon(channel: "All" | CommItem["channel"]) { return "phone"; } +function formatInboxLabel(inbox: ContactInbox) { + const title = String(inbox.title ?? "").trim(); + if (title) return `${inbox.channel} · ${title}`; + const source = String(inbox.sourceExternalId ?? "").trim(); + if (!source) return inbox.channel; + const tail = source.length > 18 ? source.slice(-18) : source; + return `${inbox.channel} · ${tail}`; +} + +function isInboxToggleLoading(inboxId: string) { + return Boolean(inboxToggleLoadingById.value[inboxId]); +} + +async function setInboxHidden(inboxId: string, hidden: boolean) { + const id = String(inboxId ?? "").trim(); + if (!id || isInboxToggleLoading(id)) return; + inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: true }; + try { + await gqlFetch<{ setContactInboxHidden: { ok: boolean } }>(setContactInboxHiddenMutation, { + inboxId: id, + hidden, + }); + await refreshCrmData(); + } finally { + inboxToggleLoadingById.value = { ...inboxToggleLoadingById.value, [id]: false }; + } +} + function messageDeliveryUiState(item: CommItem): "none" | "sending" | "sent" | "delivered" | "failed" { if (item.kind !== "message" || item.direction !== "out") return "none"; const rawStatus = String(item.deliveryStatus ?? "").toUpperCase(); @@ -5486,6 +5561,31 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
+
+

Hidden sources

+
+
+

+ {{ inbox.contactName }} · {{ formatInboxLabel(inbox) }} +

+ +
+
+
+ +
+
+ {{ formatInboxLabel(inbox) }} + +
+
+