feat: add telegram business settings in user dropdown

This commit is contained in:
Ruslan Bakiev
2026-02-22 07:40:19 +07:00
parent fa1231df37
commit 89ce62e1ee

View File

@@ -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<TelegramConnectStatus>("not_connected");
const telegramConnectStatusLoading = ref(false);
const telegramConnectBusy = ref(false);
const telegramRefreshBusy = ref(false);
const telegramConnectUrl = ref("");
const telegramConnections = ref<TelegramConnectionSummary[]>([]);
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")
</div>
<span class="max-w-[160px] truncate text-xs font-medium">{{ authDisplayName }}</span>
</button>
<ul tabindex="0" class="menu dropdown-content z-30 mt-2 w-52 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
<li class="menu-title px-2 py-1">
<span>{{ authDisplayName }}</span>
</li>
<li><button @click="logout">Logout</button></li>
</ul>
<div tabindex="0" class="dropdown-content z-30 mt-2 w-80 rounded-box border border-base-300 bg-base-100 p-3 shadow-lg">
<div class="mb-2 border-b border-base-300 pb-2">
<p class="truncate text-sm font-semibold">{{ authDisplayName }}</p>
<p class="text-[11px] uppercase tracking-wide text-base-content/60">Settings</p>
</div>
<div class="space-y-2 rounded-lg border border-base-300 bg-base-50/40 p-2">
<div class="flex items-center justify-between gap-2">
<span class="text-xs font-medium">Telegram Business</span>
<span class="badge badge-xs" :class="telegramStatusBadgeClass">{{ telegramStatusLabel }}</span>
</div>
<div class="grid grid-cols-2 gap-1.5">
<button
class="btn btn-xs btn-primary"
:disabled="telegramConnectBusy"
@click="startTelegramBusinessConnect"
>
{{ telegramConnectBusy ? "Connecting..." : "Connect" }}
</button>
<button
class="btn btn-xs btn-ghost border border-base-300"
:disabled="telegramConnectStatusLoading"
@click="loadTelegramConnectStatus"
>
{{ telegramConnectStatusLoading ? "Refreshing..." : "Refresh status" }}
</button>
<button
class="btn btn-xs btn-ghost border border-base-300"
:disabled="!telegramConnectUrl"
@click="openTelegramConnectUrl"
>
Open link
</button>
<button
class="btn btn-xs btn-ghost border border-base-300"
:disabled="!primaryTelegramBusinessConnectionId || telegramRefreshBusy"
@click="refreshTelegramBusinessConnectionFromApi"
>
{{ telegramRefreshBusy ? "Syncing..." : "Refresh from API" }}
</button>
</div>
</div>
<div class="mt-3 border-t border-base-300 pt-2">
<button class="btn btn-sm w-full btn-ghost justify-start" @click="logout">Logout</button>
</div>
</div>
</div>
</div>
</div>
@@ -3956,15 +4118,17 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</div>
<div v-else-if="calendarView === 'week'" class="space-y-2">
<div v-else-if="calendarView === 'week'" class="calendar-week-scroll overflow-x-auto pb-1">
<div class="calendar-week-grid">
<article
v-for="day in weekDays"
:key="day.key"
class="group relative rounded-xl border border-base-300 p-3"
class="group relative flex min-h-[18rem] flex-col rounded-xl border border-base-300 bg-base-100 p-2.5"
:class="selectedDateKey === day.key ? 'border-primary bg-primary/5' : ''"
@click="pickDate(day.key)"
>
<p class="mb-2 text-sm font-semibold">{{ day.label }} {{ day.day }}</p>
<div class="mb-2 flex items-start justify-between gap-2">
<p class="text-sm font-semibold leading-tight">{{ day.label }} {{ day.day }}</p>
<button
type="button"
class="calendar-hover-jump calendar-hover-jump-week"
@@ -3976,20 +4140,22 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<path d="M10 3v14M6 7l4-4 4 4M6 13l4 4 4-4" />
</svg>
</button>
<div class="space-y-1">
</div>
<div class="space-y-1.5">
<button
v-for="event in day.events"
:key="event.id"
class="block w-full rounded px-2 py-1 text-left text-xs"
class="block w-full rounded-md px-2 py-1.5 text-left text-xs"
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 ring-1 ring-success/45' : 'bg-base-200 hover:bg-base-300/80'"
@click.stop="openThreadFromCalendarItem(event)"
>
{{ formatTime(event.start) }} - {{ event.title }} ({{ event.contact }})
</button>
<p v-if="day.events.length === 0" class="text-xs text-base-content/50">No events</p>
<p v-if="day.events.length === 0" class="pt-1 text-xs text-base-content/50">No events</p>
</div>
</article>
</div>
</div>
<div v-else-if="calendarView === 'day'" class="space-y-2">
<button
@@ -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;