feat: add telegram business settings in user dropdown
This commit is contained in:
258
frontend/app.vue
258
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<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,39 +4118,43 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="calendarView === 'week'" class="space-y-2">
|
||||
<article
|
||||
v-for="day in weekDays"
|
||||
:key="day.key"
|
||||
class="group relative rounded-xl border border-base-300 p-3"
|
||||
: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>
|
||||
<button
|
||||
type="button"
|
||||
class="calendar-hover-jump calendar-hover-jump-week"
|
||||
title="Expand day line"
|
||||
aria-label="Expand day line"
|
||||
@click.stop="openDayView(day.key)"
|
||||
<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 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)"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-none stroke-current" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M10 3v14M6 7l4-4 4 4M6 13l4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="event in day.events"
|
||||
:key="event.id"
|
||||
class="block w-full rounded px-2 py-1 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>
|
||||
</div>
|
||||
</article>
|
||||
<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"
|
||||
title="Expand day line"
|
||||
aria-label="Expand day line"
|
||||
@click.stop="openDayView(day.key)"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-none stroke-current" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M10 3v14M6 7l4-4 4 4M6 13l4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<button
|
||||
v-for="event in day.events"
|
||||
:key="event.id"
|
||||
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="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">
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user