-
{{$t('login__or_continue_as')}}
+
+
+ {{ $t('login__or_continue_as') }}
+
-
+
@@ -137,19 +136,17 @@
import { ref, computed, inject, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import baseLogo from 'components/BaseLogo.vue'
- import { useI18n } from "vue-i18n"
+ import { useInputRules } from 'composables/useInputRules'
import { useAuthStore } from 'stores/auth'
import type { WebApp } from '@twa-dev/types'
import { QInput } from 'quasar'
- type ValidationRule = (val: string) => boolean | string
-
const tg = inject('tg') as WebApp
const tgUser = tg.initDataUnsafe.user
const authStore = useAuthStore()
const router = useRouter()
- const { t } = useI18n()
+ const { inputRules } = useInputRules()
const login = ref('')
const password = ref('')
@@ -159,8 +156,8 @@
const passwordInput = ref>()
const validationRules = {
- email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule],
- password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule]
+ email: [inputRules.email],
+ password: [inputRules.password]
}
const isEmailValid = computed(() =>
@@ -172,7 +169,7 @@
)
const passwordHint = computed(() => {
- const result = validationRules.password[0](password.value)
+ const result = validationRules.password[0] ? validationRules.password[0](password.value) : null
return typeof result === 'string' ? result : ''
})
@@ -213,7 +210,7 @@
async function createAccount() {
sessionStorage.setItem('pendingLogin', login.value)
- await router.push({ name: 'agreements', query: { method: 'email', type: 'register' }})
+ await router.push({ name: 'create_account' })
}
const isTelegramApp = computed(() => {
@@ -226,7 +223,7 @@
await authStore.loginWithTelegram(tg.initData)
await router.push({ name: 'projects' })
} catch {
- await router.push({ name: 'agreements', query: { method: 'telegram', type: 'register' }})
+ await router.push({ name: 'agreements', query: { type: 'register' }})
}
}
diff --git a/src/pages/ProjectsPage.vue b/src/pages/ProjectsPage.vue
index a9e36e5..91375ce 100644
--- a/src/pages/ProjectsPage.vue
+++ b/src/pages/ProjectsPage.vue
@@ -156,6 +156,7 @@
+
-
+
diff --git a/src/pages/project-page/ProjectPageUsers.vue b/src/pages/project-page/ProjectPageUsers.vue
index 9f17e88..c11421b 100644
--- a/src/pages/project-page/ProjectPageUsers.vue
+++ b/src/pages/project-page/ProjectPageUsers.vue
@@ -4,20 +4,12 @@
:hideHeader="users.length === 0"
>
-
-
-
-
-
-
-
+
@@ -37,32 +29,7 @@
class="w100"
@click="goUserInfo(item.id)"
>
-
-
-
-
-
-
- {{ $t(getUserStatus(item)?.text ?? '') }}
-
-
-
- {{item.section1}}
-
-
- {{item.section3}}
-
-
-
-
-
{{item.section2_1}}
-
{{'@' + item.section2_2}}
-
-
-
+
@@ -102,27 +69,7 @@
:key="item.id"
class="w100 text-grey"
>
-
-
-
-
-
- {{item.section1}}
-
-
- {{item.section3}}
-
-
-
-
-
{{item.section2_1}}
-
{{'@' + item.section2_2}}
-
-
-
+
@@ -130,7 +77,6 @@
-
-
-
-
-
- {{item.section1}}
-
-
- {{item.section3}}
-
-
-
-
-
{{item.section2_1}}
-
{{'@' + item.section2_2}}
-
-
-
+
@@ -246,7 +172,8 @@
import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies'
import { useUserSection } from 'composables/useUserSection'
- import { getUserStatus } from 'utils/users'
+ import pnUserListItemBody from 'components/pnUserListItemBody.vue'
+ import pnActionBar from 'components/pnActionBar.vue'
defineOptions({ inheritAttrs: false })
diff --git a/src/router/index.ts b/src/router/index.ts
index 93b6f61..4aa7349 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -33,7 +33,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE)
})
- const publicPath = ['404', 'terms', 'privacy', 'consent']
+ const publicPath = ['404', 'terms', 'privacy', 'consent', 'settings']
Router.beforeEach(async (to) => {
if (to.name !== 'telegram_only' && sessionStorage.getItem('isTelegram') === 'false') {
@@ -50,8 +50,9 @@ export default defineRouter(function (/* { store, ssrContext } */) {
return authStore.isAuth ? { name: 'projects', replace: true } : true
if (to.name === 'agreements') return true
-
if (to.name === 'create_account' && !authStore.isAuth) return true
+ if (to.name === 'recovery_password' && !authStore.isAuth) return true
+
if (!authStore.isAuth) return { name: 'login', replace: true }
diff --git a/src/router/routes.ts b/src/router/routes.ts
index 78cf0cb..1c8ef84 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -138,9 +138,9 @@ const routes: RouteRecordRaw[] = [
component: () => import('pages/account/ConsentPage.vue')
},
{
- name: 'change_plan_rules',
- path: '/change-plan-rules',
- component: () => import('pages/account/ChangePlanRules.vue')
+ name: 'change_tariff_rules',
+ path: '/change-tariff-rules',
+ component: () => import('src/pages/account/ChangeTariffRules.vue')
},
{
name: 'support',
diff --git a/src/stores/SSE.ts b/src/stores/SSE.ts
index d142220..52a4cbd 100644
--- a/src/stores/SSE.ts
+++ b/src/stores/SSE.ts
@@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { api } from 'boot/axios'
import { useProjectsStore } from 'stores/projects'
+import { useAuthStore } from 'stores/auth'
import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
@@ -12,6 +13,7 @@ export const useSSEStore = defineStore('sse', () => {
const maxReconnectAttempts = 5
const projectsStore = useProjectsStore()
+ const authStore = useAuthStore()
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
@@ -61,33 +63,30 @@ export const useSSEStore = defineStore('sse', () => {
const setupEventListeners = () => {
if (!eventSource.value) return
- eventSource.value.addEventListener('chat-attach', (event) => {
+ eventSource.value.addEventListener('chat-update', (event) => {
const data = JSON.parse(event.data)
+ projectsStore.init().catch(() => {}) // for update count of chats on projects page
+ authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
if (data.project_id === currentProjectId.value) {
chatsStore.init().catch(() => {})
+ usersStore.init().catch(() => {}) // for update users from chats
}
})
eventSource.value.addEventListener('chat-delete', (event) => {
const data = JSON.parse(event.data)
+ projectsStore.init().catch(() => {}) // for update count of chats on projects page
+ authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
if (data.project_id === currentProjectId.value) {
chatsStore.init().catch(() => {})
- usersStore.init().catch(() => {})
+ usersStore.init().catch(() => {}) // for update users from chats
}
})
- eventSource.value.addEventListener('chat-detach', (event) => {
+ eventSource.value.addEventListener('customer-update', (event) => {
const data = JSON.parse(event.data)
if (data.project_id === currentProjectId.value) {
- chatsStore.detach(data.id)
- usersStore.init().catch(() => {})
- }
- })
-
- eventSource.value.addEventListener('chat-update', (event) => {
- const data = JSON.parse(event.data)
- if (data.project_id === currentProjectId.value) {
- chatsStore.init().catch(() => {})
+ authStore.updateProfile().catch(() => {})
}
})
}
diff --git a/src/stores/auth.ts b/src/stores/auth.ts
index d76cd79..9e58ab2 100644
--- a/src/stores/auth.ts
+++ b/src/stores/auth.ts
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { api } from 'boot/axios'
import type { CompanyParams } from 'types/Company'
+import type { CurrentTariff } from 'types/Tariff'
import { useProjectsStore } from 'stores/projects'
interface Customer {
@@ -12,6 +13,7 @@ interface Customer {
last_name?: string
avatar?: string
company?: CompanyParams
+ plan?: CurrentTariff
}
interface WSMessage {
@@ -24,7 +26,7 @@ interface WSMessage {
const ENDPOINT_MAP = {
register: '/auth/email/register',
- forgot: '/auth/forgot',
+ forgot: '/auth/email/recovery',
changePwd: '/auth/email/change-password',
changeMethod: '/auth/email/upgrade'
} as const
@@ -104,6 +106,12 @@ export const useAuthStore = defineStore('auth', () => {
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) =>
await api.post('/auth/email/change-email', { code, code2, email, password })
+ // for payment
+ async function updateProfile () {
+ const { data } = await api.get('/customer/profile')
+ customer.value = data.data
+ }
+
// user data company
const updateMyCompany = async (companyData: CompanyParams) => {
const response = await api.put('/customer/profile', { company: companyData })
@@ -131,6 +139,7 @@ export const useAuthStore = defineStore('auth', () => {
initRegistration,
confirmCode,
setPassword,
+ updateProfile,
updateMyCompany,
getCodeCurrentEmail,
confirmCurrentEmailCode,
diff --git a/src/stores/chats.ts b/src/stores/chats.ts
index c5a0daa..36cd06c 100644
--- a/src/stores/chats.ts
+++ b/src/stores/chats.ts
@@ -1,22 +1,35 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { api } from 'boot/axios'
-import type { Chat } from 'types/Chat'
+import type { Chat, ObsoleteChat } from 'types/Chat'
import { useProjectsStore } from 'stores/projects'
import { useUsersStore } from 'stores/users'
export const useChatsStore = defineStore('chats', () => {
const projectsStore = useProjectsStore()
const chats = ref
([])
- const isInit = ref(false)
+ const obsoleteChats = ref([])
+ const isInit = ref(false)
+ const isLoading = ref(false)
const currentProjectId = computed(() => projectsStore.currentProjectId)
async function init () {
- reset()
- const { data } = await api.get('/project/' + currentProjectId.value + '/chat')
- chats.value.push(...data.data)
- isInit.value = true
+ // guard for double request
+ if (isLoading.value) return
+ isLoading.value = true
+ try {
+ reset()
+ const chatsResponse = await api.get(`/project/${currentProjectId.value}/chat`)
+ chats.value = chatsResponse.data.data
+ const obsoleteChatsResponse = await api.get(`/project/${currentProjectId.value}/chat/obsolete`)
+ obsoleteChats.value = obsoleteChatsResponse.data.data
+ isInit.value = true
+ } catch {
+ reset()
+ } finally {
+ isLoading.value = false
+ }
}
function reset () {
@@ -28,6 +41,8 @@ export const useChatsStore = defineStore('chats', () => {
const { data } = await api.delete('/project/' + currentProjectId.value + '/chat/' + chatId)
chats.value = chats.value.filter(item => item.id !== data.data.id)
const usersStore = useUsersStore()
+ reset()
+ await init()
await usersStore.init()
}
@@ -39,31 +54,15 @@ export const useChatsStore = defineStore('chats', () => {
function chatById (id: number) {
return chats.value.find(el => el.id === id)
}
-
- // attach, update and detach for use in SSE-store
- function attach (chat: Chat) {
- chats.value.push(chat)
- }
-
- function update (chat: Chat) {
- const idx = chats.value.findIndex(item => item.id === chat.id)
- if (chats.value[idx]) Object.assign(chats.value[idx], chat)
- }
-
- function detach (chatId: number) {
- chats.value = chats.value.filter(item => item.id !== chatId)
- }
-
+
return {
chats,
+ obsoleteChats,
isInit,
init,
reset,
unlink,
getKey,
- chatById,
- attach,
- update,
- detach
+ chatById
}
})
diff --git a/src/stores/projects.ts b/src/stores/projects.ts
index 934ab10..86d997f 100644
--- a/src/stores/projects.ts
+++ b/src/stores/projects.ts
@@ -15,13 +15,23 @@ export const useProjectsStore = defineStore('projects', () => {
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const companiesStore = useCompaniesStore()
+ const isLoading = ref(false)
-
async function init () {
- const { data } = await api.get('/project')
- const projectsAPI = data.data
- projects.value.push(...projectsAPI)
- isInit.value = true
+ // guard for double request
+ if (isLoading.value) return
+ isLoading.value = true
+ try {
+ reset()
+ const { data } = await api.get('/project')
+ const projectsAPI = data.data
+ projects.value.push(...projectsAPI)
+ isInit.value = true
+ } catch {
+ reset()
+ } finally {
+ isLoading.value = false
+ }
}
function reset () {
@@ -31,7 +41,7 @@ export const useProjectsStore = defineStore('projects', () => {
}
async function add (projectData: ProjectParams) {
- const { data }= await api.post('/project', projectData)
+ const { data } = await api.post('/project', projectData)
const newProjectAPI = data.data
projects.value.push(newProjectAPI)
return newProjectAPI
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index efdaea8..81ad424 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -3,6 +3,7 @@ import { ref, computed, inject } from 'vue'
import { api } from 'boot/axios'
import { useAuthStore } from 'stores/auth'
import { useI18n } from 'vue-i18n'
+import { Lang } from 'quasar'
import type { WebApp } from '@twa-dev/types'
interface AppSettings {
@@ -41,6 +42,24 @@ export const useSettingsStore = defineStore('settings', () => {
{ value: 16, label: 'settings__fontsize_medium' },
{ value: 20, label: 'settings__fontsize_large' }
]
+
+ const quasarLangMap: Record = {
+ 'en-US': 'en-US',
+ 'ru-RU': 'ru'
+ }
+
+ const updateQuasarLang = async (locale: string) => {
+ const quasarLang = quasarLangMap[locale] || 'en-US'
+
+ try {
+ const langModule = await import(
+ `../../node_modules/quasar/lang/${quasarLang}.js`
+ )
+ Lang.set(langModule.default)
+ } catch (e) {
+ console.error('Quasar Error load locale:', quasarLang, e)
+ }
+ }
const detectLocale = (): string => {
const localeMap = {
@@ -69,9 +88,10 @@ export const useSettingsStore = defineStore('settings', () => {
)
}
- const applyLocale = () => {
+ const applyLocale = async () => {
if (settings.value.locale && i18nLocale) {
i18nLocale.value = settings.value.locale
+ await updateQuasarLang(settings.value.locale)
}
}
@@ -104,19 +124,15 @@ export const useSettingsStore = defineStore('settings', () => {
}
}
updateCssVariable()
- applyLocale()
+ await applyLocale()
isInit.value = true
}
- const saveSettings = async () => {
- await api.put('/customer/settings', settings.value)
- }
-
const updateSettings = async (newSettings: Partial) => {
settings.value = { ...settings.value, ...newSettings }
updateCssVariable()
- applyLocale()
- await saveSettings()
+ await applyLocale()
+ if (authStore.isAuth) await api.put('/customer/settings', settings.value)
}
return {
diff --git a/src/stores/users.ts b/src/stores/users.ts
index a18b3b0..cd96b4a 100644
--- a/src/stores/users.ts
+++ b/src/stores/users.ts
@@ -8,15 +8,24 @@ export const useUsersStore = defineStore('users', () => {
const projectsStore = useProjectsStore()
const users = ref([])
const isInit = ref(false)
+ const isLoading = ref(false)
const currentProjectId = computed(() => projectsStore.currentProjectId)
async function init () {
- reset()
- const { data } = await api.get(`/project/${currentProjectId.value}/user`)
- const usersAPI = data.data
- users.value.push(...usersAPI)
- isInit.value = true
+ // guard for double request
+ if (isLoading.value) return
+ isLoading.value = true
+ try {
+ reset()
+ const { data } = await api.get(`/project/${currentProjectId.value}/user`)
+ users.value = data.data
+ isInit.value = true
+ } catch {
+ reset()
+ } finally {
+ isLoading.value = false
+ }
}
function reset () {
@@ -28,7 +37,7 @@ export const useUsersStore = defineStore('users', () => {
const { data } = await api.put(`/project/${currentProjectId.value}/user/${userId}`, userData)
const userAPI = data.data
const idx = users.value.findIndex(item => item.id === userAPI.id)
- if (users.value[idx]) Object.assign(users.value[idx], userAPI)
+ if (idx !== -1) users.value[idx] = { ...users.value[idx], ...userAPI }
}
async function blockUser (userId: number) {
@@ -48,7 +57,7 @@ export const useUsersStore = defineStore('users', () => {
}
function userById (id: number) {
- return users.value.find(el =>el.id === id)
+ return users.value.find(el => el.id === id)
}
function userNameById (id: number) {
diff --git a/src/types/Chat.ts b/src/types/Chat.ts
index a7b6b5f..496b25f 100644
--- a/src/types/Chat.ts
+++ b/src/types/Chat.ts
@@ -5,7 +5,6 @@ interface Chat {
message_id: number
name: string
is_channel: boolean
- bot_can_ban: boolean
user_count: number
last_update_time: number
description: string | null
@@ -14,20 +13,24 @@ interface Chat {
bot_access: 0 | 1 | 2
bot_access_date: number
bot_rights: 1 | 2 | 3
- restore_date: null | number
+ state: 1 | 2 | 3 | 4
+ state_date: null | number
users: number []
last_pts?: number
[key: string]: number | string | boolean | null | number[]
}
-type ChatType =
- 0 | 'detach' |
- 1 | 'notify' |
- 2 | 'full' |
- 9 | 'delete' |
- 10 | 'reconnect'
+interface ObsoleteChat {
+ id: number
+ name: string
+ state: 1 | 2 | 3 | 4
+ state_date: null | number
+ telegram_id: string
+ logo: string | null
+ [key: string]: number | string | null
+}
export type {
Chat,
- ChatType
+ ObsoleteChat
}
diff --git a/src/types/Tariff.ts b/src/types/Tariff.ts
new file mode 100644
index 0000000..f4562cb
--- /dev/null
+++ b/src/types/Tariff.ts
@@ -0,0 +1,32 @@
+interface Tariff {
+ id: number
+ name: string
+ label: string
+ chat_limit: number | null
+ price: number
+ is_archived: boolean
+ [key: string]: string | boolean | number | null
+}
+
+interface CurrentTariff {
+ id: number
+ can_change: boolean
+ debt: number
+ used_chats: number
+ start_date: number
+ end_date: number | null
+ chat_limit: number | null
+}
+
+interface TariffPeriod {
+ id: number
+ name: string
+ name_in_days: string
+ discount: number
+}
+
+export type {
+ Tariff,
+ CurrentTariff,
+ TariffPeriod
+}
diff --git a/src/utils/chats.ts b/src/utils/chats.ts
index ba63624..0763010 100644
--- a/src/utils/chats.ts
+++ b/src/utils/chats.ts
@@ -1,50 +1,84 @@
-import type { ChatType } from "types/Chat"
+function chatAccess (i: 0 | 1 | 2 ) {
+ switch (i) {
+ case 0: return typeDescription('no_access')
+ case 1: return typeDescription('notify')
+ case 2: return typeDescription('full')
+ }
-function getChatType (type: ChatType) {
+}
+
+function chatState (i: 1 | 2 | 3 | 4) {
+ switch (i) {
+ case 1: return typeDescription('new')
+ case 2: return typeDescription('reconnect')
+ case 3: return typeDescription('detach')
+ case 4: return typeDescription('delete')
+ }
+}
+
+type ChatType = 'no_access' | 'notify' | 'full' | 'new' | 'reconnect' | 'detach' | 'delete'
+
+function typeDescription (type: ChatType) {
switch (type) {
- default:
- case 0:
- case 'detach': return {
- icon: 'mdi-link-off',
+ case 'no_access': return {
+ info: 'no_access',
+ icon: 'mdi-close',
color: 'negative',
- label: 'chat_type__detach_label',
- description: 'chat_type__detach_description'
+ label: 'chat_type__label_no_access',
+ description: 'chat_type__description_no_access'
}
- case 1:
- case 'notify': return {
+ case 'notify': return {
+ info: type,
icon: 'mdi-bell-outline',
color: 'info',
- label: 'chat_type__notify_label',
- description: 'chat_type__notify_description'
+ label: 'chat_type__label_notify',
+ description: 'chat_type__description_notify'
}
- case 2:
- case 'full': return {
- icon: 'mdi-star-shooting-outline',
+ case 'full': return {
+ info: type,
+ icon: 'mdi-crown-outline',
color: 'primary',
- label: 'chat_type__full_label',
- description: 'chat_type__full_description'
+ label: 'chat_type__label_full',
+ description: 'chat_type__description_full'
}
- case 9:
- case 'delete': return {
- icon: 'mdi-forum-remove-outline',
- color: 'negative',
- label: 'chat_type__delete_label',
- description: 'chat_type__delete_description'
+ case 'new': return {
+ info: type,
+ icon: '',
+ color: 'primary',
+ label: 'chat_type__label_new',
+ description: 'chat_type__description_new'
}
- case 10:
- case 'reconnect': return {
+ case 'reconnect': return {
+ info: type,
icon: 'mdi-repeat-variant',
color: 'negative',
- label: 'chat_type__reconnect_label',
- description: 'chat_type__reconnect_description'
+ label: 'chat_type__label_reconnect',
+ description: 'chat_type__description_reconnect'
+ }
+
+ case 'detach': return {
+ info: type,
+ icon: 'mdi-link-off',
+ color: 'negative',
+ label: 'chat_type__label_detach',
+ description: 'chat_type__description_detach'
+ }
+
+ case 'delete': return {
+ info: type,
+ icon: 'mdi-chat-remove-outline',
+ color: 'negative',
+ label: 'chat_type__label_delete',
+ description: 'chat_type__description_delete'
}
}
}
export {
- getChatType
+ chatAccess,
+ chatState
}
\ No newline at end of file
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 6ea10cf..de8a789 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -4,17 +4,35 @@ function parseIntString (s: string | string[] | undefined) :number | null {
return regex.test(s) ? Number(s) : null
}
-const removeNullKeys = (obj: T): Partial =>
- Object.fromEntries(
- Object.entries(obj).filter(([, v]) => v !== null)
- ) as Partial
+type RemoveNullKeysReturn =
+ Partial>> & Pick
+
+const removeNullKeys = (
+ obj: T,
+ exceptions?: readonly K[]
+): RemoveNullKeysReturn => {
+ const result: Partial = {}
+
+ for (const [key, value] of Object.entries(obj) as [keyof T, T[keyof T]][]) {
+ if (exceptions?.includes(key as K) || value !== null) {
+ result[key] = value
+ }
+ }
+
+ return result as RemoveNullKeysReturn
+}
const getFieldValue = (mod: string, props: undefined | null | string ) => {
return mod !== '' ? mod : props === null || props === undefined ? null : ''
}
+const formatNumber = (number: string | number) => {
+ return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
+}
+
export {
parseIntString,
removeNullKeys,
- getFieldValue
+ getFieldValue,
+ formatNumber
}