diff --git a/frontend/app.vue b/frontend/app.vue index 77c4afc..107ea39 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -577,6 +577,118 @@ const authInitials = computed(() => { return parts.map((part) => part[0]?.toUpperCase() ?? "").join(""); }); +type TelegramConnectStatus = + | "not_connected" + | "pending_link" + | "pending_business_connection" + | "connected" + | "disabled" + | "no_reply_rights"; + +type TelegramConnectionSummary = { + businessConnectionId: string; + isEnabled: boolean | null; + canReply: boolean | null; + updatedAt: string; +}; + +const telegramConnectStatus = ref("not_connected"); +const telegramConnectStatusLoading = ref(false); +const telegramConnectBusy = ref(false); +const telegramRefreshBusy = ref(false); +const telegramConnectUrl = ref(""); +const telegramConnections = ref([]); + +const telegramStatusLabel = computed(() => { + if (telegramConnectStatusLoading.value) return "Checking"; + if (telegramConnectStatus.value === "connected") return "Connected"; + if (telegramConnectStatus.value === "pending_link") return "Pending link"; + if (telegramConnectStatus.value === "pending_business_connection") return "Waiting business connect"; + if (telegramConnectStatus.value === "disabled") return "Disabled"; + if (telegramConnectStatus.value === "no_reply_rights") return "No reply rights"; + return "Not connected"; +}); + +const telegramStatusBadgeClass = computed(() => { + if (telegramConnectStatus.value === "connected") return "badge-success"; + if (telegramConnectStatus.value === "pending_link" || telegramConnectStatus.value === "pending_business_connection") return "badge-warning"; + if (telegramConnectStatus.value === "disabled" || telegramConnectStatus.value === "no_reply_rights") return "badge-error"; + return "badge-ghost"; +}); + +const primaryTelegramBusinessConnectionId = computed( + () => telegramConnections.value.find((item) => (item.businessConnectionId ?? "").trim())?.businessConnectionId ?? "", +); + +async function loadTelegramConnectStatus() { + if (!authMe.value) { + telegramConnectStatus.value = "not_connected"; + telegramConnections.value = []; + telegramConnectUrl.value = ""; + return; + } + + telegramConnectStatusLoading.value = true; + try { + const result = await $fetch<{ + ok: boolean; + status: TelegramConnectStatus; + connections?: TelegramConnectionSummary[]; + }>("/api/omni/telegram/business/connect/status", { + method: "GET", + }); + telegramConnectStatus.value = result?.status ?? "not_connected"; + telegramConnections.value = result?.connections ?? []; + } catch { + telegramConnectStatus.value = "not_connected"; + telegramConnections.value = []; + } finally { + telegramConnectStatusLoading.value = false; + } +} + +async function startTelegramBusinessConnect() { + if (telegramConnectBusy.value) return; + telegramConnectBusy.value = true; + try { + const result = await $fetch<{ + ok: boolean; + status: TelegramConnectStatus; + connectUrl: string; + expiresAt: string; + }>("/api/omni/telegram/business/connect/start", { method: "POST" }); + telegramConnectStatus.value = result?.status ?? "pending_link"; + telegramConnectUrl.value = String(result?.connectUrl ?? "").trim(); + if (telegramConnectUrl.value && process.client) { + window.open(telegramConnectUrl.value, "_blank", "noopener,noreferrer"); + } + } finally { + telegramConnectBusy.value = false; + await loadTelegramConnectStatus(); + } +} + +function openTelegramConnectUrl() { + if (!telegramConnectUrl.value || !process.client) return; + window.open(telegramConnectUrl.value, "_blank", "noopener,noreferrer"); +} + +async function refreshTelegramBusinessConnectionFromApi() { + const businessConnectionId = primaryTelegramBusinessConnectionId.value; + if (!businessConnectionId || telegramRefreshBusy.value) return; + + telegramRefreshBusy.value = true; + try { + await $fetch<{ ok: boolean }>("/api/omni/telegram/business/connect/refresh", { + method: "POST", + body: { businessConnectionId }, + }); + } finally { + telegramRefreshBusy.value = false; + await loadTelegramConnectStatus(); + } +} + function pilotToUiMessage(message: PilotMessage): UIMessage { return { id: message.id, @@ -719,13 +831,19 @@ async function bootstrapSession() { if (!authMe.value) { pilotMessages.value = []; chatConversations.value = []; + telegramConnectStatus.value = "not_connected"; + telegramConnections.value = []; + telegramConnectUrl.value = ""; return; } - await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); + await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData(), loadTelegramConnectStatus()]); } catch { authMe.value = null; pilotMessages.value = []; chatConversations.value = []; + telegramConnectStatus.value = "not_connected"; + telegramConnections.value = []; + telegramConnectUrl.value = ""; } finally { authResolved.value = true; } @@ -776,7 +894,7 @@ async function login() { }); await loadMe(); startPilotBackgroundPolling(); - await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); + await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData(), loadTelegramConnectStatus()]); } catch (e: any) { loginError.value = e?.data?.message || e?.message || "Login failed"; } finally { @@ -793,6 +911,9 @@ async function logout() { livePilotAssistantText.value = ""; pilotChat.messages = []; chatConversations.value = []; + telegramConnectStatus.value = "not_connected"; + telegramConnections.value = []; + telegramConnectUrl.value = ""; } async function refreshCrmData() { @@ -3805,12 +3926,53 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") {{ authDisplayName }} - + @@ -3956,39 +4118,43 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") -
-
-

{{ day.label }} {{ day.day }}

- -
- -

No events

-
-
+
+

{{ day.label }} {{ day.day }}

+ +
+
+ +

No events

+
+ +
@@ -5126,6 +5292,13 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") height: 100%; } +.calendar-week-grid { + display: grid; + grid-template-columns: repeat(7, minmax(165px, 1fr)); + gap: 8px; + min-width: 1180px; +} + .calendar-side-nav { position: absolute; top: 50%; @@ -5233,6 +5406,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") padding-right: 32px; } + .calendar-week-grid { + grid-template-columns: repeat(7, minmax(150px, 1fr)); + min-width: 1060px; + } + .calendar-side-nav { width: 24px; height: 24px;