refactor(frontend): extract auth and topbar workspace components
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||||
|
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
||||||
|
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||||
|
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
||||||
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
||||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
||||||
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
|
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
|
||||||
@@ -4725,38 +4728,18 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
|
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
|
||||||
<div v-if="!authResolved" class="flex h-full items-center justify-center">
|
<CrmAuthLoading v-if="!authResolved" />
|
||||||
<span class="loading loading-spinner loading-md text-base-content/70" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="!authMe" class="flex h-full items-center justify-center px-3">
|
<CrmAuthLoginForm
|
||||||
<div class="card w-full max-w-sm border border-base-300 bg-base-100 shadow-sm">
|
v-else-if="!authMe"
|
||||||
<div class="card-body p-5">
|
:phone="loginPhone"
|
||||||
<h1 class="text-lg font-semibold">Login</h1>
|
:password="loginPassword"
|
||||||
<p class="mt-1 text-xs text-base-content/65">Sign in with phone and password.</p>
|
:error="loginError"
|
||||||
<div class="mt-4 space-y-2">
|
:busy="loginBusy"
|
||||||
<input
|
@update:phone="loginPhone = $event"
|
||||||
v-model="loginPhone"
|
@update:password="loginPassword = $event"
|
||||||
type="tel"
|
@submit="login"
|
||||||
class="input input-bordered w-full"
|
/>
|
||||||
placeholder="+1 555 000 0001"
|
|
||||||
@keyup.enter="login"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="loginPassword"
|
|
||||||
type="password"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
placeholder="Password"
|
|
||||||
@keyup.enter="login"
|
|
||||||
>
|
|
||||||
<p v-if="loginError" class="text-xs text-error">{{ loginError }}</p>
|
|
||||||
<button class="btn w-full" :disabled="loginBusy" @click="login">
|
|
||||||
{{ loginBusy ? "Logging in..." : "Login" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
|
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||||
@@ -5011,83 +4994,21 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
|
|
||||||
<main class="relative min-h-0 bg-base-100">
|
<main class="relative min-h-0 bg-base-100">
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
<div class="workspace-topbar border-b border-base-300 px-3 py-2 md:px-4">
|
<CrmWorkspaceTopbar
|
||||||
<div class="flex items-center justify-between gap-3">
|
:selected-tab="selectedTab"
|
||||||
<div class="join">
|
:people-left-mode="peopleLeftMode"
|
||||||
<button
|
:auth-initials="authInitials"
|
||||||
class="btn btn-sm join-item"
|
:auth-display-name="authDisplayName"
|
||||||
:class="
|
:telegram-status-badge-class="telegramStatusBadgeClass"
|
||||||
selectedTab === 'communications' && peopleLeftMode === 'contacts'
|
:telegram-status-label="telegramStatusLabel"
|
||||||
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
:telegram-connect-busy="telegramConnectBusy"
|
||||||
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
:telegram-connect-notice="telegramConnectNotice"
|
||||||
"
|
@open-contacts="setPeopleLeftMode('contacts', true)"
|
||||||
@click="setPeopleLeftMode('contacts', true)"
|
@open-calendar="setPeopleLeftMode('calendar', true)"
|
||||||
>
|
@open-documents="openDocumentsTab(true)"
|
||||||
Contacts
|
@start-telegram-connect="startTelegramBusinessConnect"
|
||||||
</button>
|
@logout="logout"
|
||||||
<button
|
/>
|
||||||
class="btn btn-sm join-item"
|
|
||||||
:class="
|
|
||||||
selectedTab === 'communications' && peopleLeftMode === 'calendar'
|
|
||||||
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
|
||||||
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
|
||||||
"
|
|
||||||
@click="setPeopleLeftMode('calendar', true)"
|
|
||||||
>
|
|
||||||
Calendar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm join-item"
|
|
||||||
:class="
|
|
||||||
selectedTab === 'documents'
|
|
||||||
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
|
||||||
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
|
||||||
"
|
|
||||||
@click="openDocumentsTab(true)"
|
|
||||||
>
|
|
||||||
Documents
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<button tabindex="0" class="btn btn-sm btn-ghost gap-2">
|
|
||||||
<div class="avatar placeholder">
|
|
||||||
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-primary-content">
|
|
||||||
<span class="text-[11px] font-semibold leading-none">{{ authInitials }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="max-w-[160px] truncate text-xs font-medium">{{ authDisplayName }}</span>
|
|
||||||
</button>
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
class="btn btn-xs btn-primary w-full"
|
|
||||||
:disabled="telegramConnectBusy"
|
|
||||||
@click="startTelegramBusinessConnect"
|
|
||||||
>
|
|
||||||
{{ telegramConnectBusy ? "Connecting..." : "Connect Telegram" }}
|
|
||||||
</button>
|
|
||||||
<p v-if="telegramConnectNotice" class="text-[11px] leading-snug text-base-content/70">
|
|
||||||
{{ telegramConnectNotice }}
|
|
||||||
</p>
|
|
||||||
</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>
|
|
||||||
<div
|
<div
|
||||||
class="min-h-0 flex-1"
|
class="min-h-0 flex-1"
|
||||||
:class="selectedTab === 'documents' || (selectedTab === 'communications' && peopleLeftMode === 'contacts') ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
:class="selectedTab === 'documents' || (selectedTab === 'communications' && peopleLeftMode === 'contacts') ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full items-center justify-center">
|
||||||
|
<span class="loading loading-spinner loading-md text-base-content/70" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
47
frontend/app/components/workspace/auth/CrmAuthLoginForm.vue
Normal file
47
frontend/app/components/workspace/auth/CrmAuthLoginForm.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
error: string | null;
|
||||||
|
busy: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:phone", value: string): void;
|
||||||
|
(e: "update:password", value: string): void;
|
||||||
|
(e: "submit"): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full items-center justify-center px-3">
|
||||||
|
<div class="card w-full max-w-sm border border-base-300 bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h1 class="text-lg font-semibold">Login</h1>
|
||||||
|
<p class="mt-1 text-xs text-base-content/65">Sign in with phone and password.</p>
|
||||||
|
<div class="mt-4 space-y-2">
|
||||||
|
<input
|
||||||
|
:value="props.phone"
|
||||||
|
type="tel"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="+1 555 000 0001"
|
||||||
|
@input="emit('update:phone', ($event.target as HTMLInputElement).value)"
|
||||||
|
@keyup.enter="emit('submit')"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:value="props.password"
|
||||||
|
type="password"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Password"
|
||||||
|
@input="emit('update:password', ($event.target as HTMLInputElement).value)"
|
||||||
|
@keyup.enter="emit('submit')"
|
||||||
|
>
|
||||||
|
<p v-if="props.error" class="text-xs text-error">{{ props.error }}</p>
|
||||||
|
<button class="btn w-full" :disabled="props.busy" @click="emit('submit')">
|
||||||
|
{{ props.busy ? "Logging in..." : "Login" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
100
frontend/app/components/workspace/header/CrmWorkspaceTopbar.vue
Normal file
100
frontend/app/components/workspace/header/CrmWorkspaceTopbar.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedTab: "communications" | "documents";
|
||||||
|
peopleLeftMode: "contacts" | "calendar";
|
||||||
|
authInitials: string;
|
||||||
|
authDisplayName: string;
|
||||||
|
telegramStatusBadgeClass: string;
|
||||||
|
telegramStatusLabel: string;
|
||||||
|
telegramConnectBusy: boolean;
|
||||||
|
telegramConnectNotice: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "open-contacts"): void;
|
||||||
|
(e: "open-calendar"): void;
|
||||||
|
(e: "open-documents"): void;
|
||||||
|
(e: "start-telegram-connect"): void;
|
||||||
|
(e: "logout"): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="workspace-topbar border-b border-base-300 px-3 py-2 md:px-4">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="join">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm join-item"
|
||||||
|
:class="
|
||||||
|
props.selectedTab === 'communications' && props.peopleLeftMode === 'contacts'
|
||||||
|
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
||||||
|
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
||||||
|
"
|
||||||
|
@click="emit('open-contacts')"
|
||||||
|
>
|
||||||
|
Contacts
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm join-item"
|
||||||
|
:class="
|
||||||
|
props.selectedTab === 'communications' && props.peopleLeftMode === 'calendar'
|
||||||
|
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
||||||
|
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
||||||
|
"
|
||||||
|
@click="emit('open-calendar')"
|
||||||
|
>
|
||||||
|
Calendar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm join-item"
|
||||||
|
:class="
|
||||||
|
props.selectedTab === 'documents'
|
||||||
|
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
|
||||||
|
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
|
||||||
|
"
|
||||||
|
@click="emit('open-documents')"
|
||||||
|
>
|
||||||
|
Documents
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<button tabindex="0" class="btn btn-sm btn-ghost gap-2">
|
||||||
|
<div class="avatar placeholder">
|
||||||
|
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-primary-content">
|
||||||
|
<span class="text-[11px] font-semibold leading-none">{{ props.authInitials }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="max-w-[160px] truncate text-xs font-medium">{{ props.authDisplayName }}</span>
|
||||||
|
</button>
|
||||||
|
<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">{{ props.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="props.telegramStatusBadgeClass">{{ props.telegramStatusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-xs btn-primary w-full"
|
||||||
|
:disabled="props.telegramConnectBusy"
|
||||||
|
@click="emit('start-telegram-connect')"
|
||||||
|
>
|
||||||
|
{{ props.telegramConnectBusy ? "Connecting..." : "Connect Telegram" }}
|
||||||
|
</button>
|
||||||
|
<p v-if="props.telegramConnectNotice" class="text-[11px] leading-snug text-base-content/70">
|
||||||
|
{{ props.telegramConnectNotice }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 border-t border-base-300 pt-2">
|
||||||
|
<button class="btn btn-sm w-full btn-ghost justify-start" @click="emit('logout')">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user