add chat-card

1. add chat card
2. fix settings (output object)
3. add input phone and email to user
4. fix empty string to null
This commit is contained in:
2025-07-29 13:26:46 +03:00
parent 462ed2b671
commit feb351424e
14 changed files with 215 additions and 118 deletions

Binary file not shown.

View File

@@ -26,18 +26,14 @@ const api = axios.create({
api.interceptors.response.use(
response => response,
async (error: AxiosError<{ error?: { code: string; message: string } }>) => {
console.log(error)
const errorData = error.response?.data?.error || {
code: 'ZERO',
message: error.message || 'Unknown error'
}
const serverError = new ServerError(
errorData.code,
errorData.message
)
const serverError = new ServerError(errorData.code, errorData.message)
if (!error.config?.suppressNotify) {
if (error.config && !(error.config as AxiosRequestConfig).suppressNotify) {
Notify.create({
type: 'negative',
message: errorData.code + ': ' + errorData.message,

View File

@@ -98,6 +98,28 @@
class = "w100 q-pt-sm"
:label = "$t('user_block__role')"
/>
<q-input
v-model.trim="modelValue.phone"
dense
filled
class = "w100 q-pt-sm"
>
<template #prepend>
<q-icon name="mdi-phone-outline"/>
</template>
</q-input>
<q-input
v-model.trim="modelValue.email"
dense
filled
class = "w100 q-pt-sm"
>
<template #prepend>
<q-icon name="mdi-email-outline"/>
</template>
</q-input>
</div>
</div>

View File

@@ -0,0 +1,37 @@
import { useCompaniesStore } from 'stores/companies'
import type { User } from 'types/Users'
export function useUserSection() {
const companiesStore = useCompaniesStore()
const userSection = (user: User) => {
const tname = () => {
return user.firstname
? user.lastname
? user.firstname + ' ' + user.lastname
: user.firstname
: user.lastname ?? ''
};
const section1 = user.fullname ?? tname()
const section2_1 = user.fullname ? tname() : ''
const section2_2 = user.username ?? ''
const section3 = (
user.company_id && companiesStore.companyById(user.company_id)
? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department) ? ' / ' : '')
: ''
) +
(user.department ? user.department + (user.role ? ' / ' : '') : '') +
(user.role ?? '')
return {
section1,
section2_1,
section2_2,
section3
}
}
return { userSection }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,91 +1,142 @@
<template>
<pn-page-card>
<template #title>
<pn-account-block-name/>
{{ $t('chat_card__title') }}
</template>
<template #footer>
<q-btn
@click="logout()"
flat
round
icon="mdi-logout"
/>
rounded color="primary"
class="w100 q-mt-md q-mb-xs"
@click = "onSubmit"
v-if="chat && chat.invite_link"
>
{{ $t("chat_card__go_chat") }}
</q-btn>
</template>
<pn-scroll-list>
<q-list separator>
<q-item
v-for="item in displayItems"
:key="item.id"
@click="goTo(item.pathName)"
clickable
v-ripple
<div
v-if="chat"
class="flex column items-center q-pa-md q-pb-lg"
>
<pn-auto-avatar
:img="chat.logo"
:name="chat.name"
size="100px"
rounded
/>
<div
v-if="chat.description"
class="flex row items-start justify-center q-pt-md"
>
<q-item-section avatar>
<q-avatar
:icon="item.icon"
:color="item.iconColor ? item.iconColor: 'brand'"
text-color="white"
rounded
font-size ="26px"
/>
</q-item-section>
<q-item-section>
<q-item-label>
{{ $t(item.name) }}
</q-item-label>
<q-item-label class="text-caption" v-if="$te(item.description)">
{{ $t(item.description) }}
</q-item-label>
</q-item-section>
{{ chat.description }}
</div>
</div>
<q-list separator v-if="chatUsers.length!==0">
<q-item-label header>
{{ $t('chat_page__members') + ' (' + chatUsers.length +')' }}
</q-item-label>
<q-item
v-for="item in chatUsers"
:key="item.id"
v-ripple
clickable
class="w100"
@click="goUserInfo(item.id)"
>
<q-item-section avatar>
<pn-auto-avatar
:img="item.photo"
:name="item.section1"
/>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold" v-if="item.section1">
<q-badge
v-if="item.is_blocked"
color="negative"
>
{{ $t('chat_page__user_blocked') }}
</q-badge>
{{item.section1}}
</q-item-label>
<q-item-label lines="1" caption v-if="item.section3">
{{item.section3}}
</q-item-label>
<q-item-label caption lines="2">
<div class="flex items-center">
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
</div>
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from 'stores/auth'
import { ref, computed, watch, inject } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies'
import { parseIntString } from 'helpers/helpers'
import type { Chat } from 'types/Chat'
import type { WebApp } from '@twa-dev/types'
import { useUserSection } from 'composables/useUserSection'
const tg = inject('tg') as WebApp
const router = useRouter()
const authStore = useAuthStore()
const route = useRoute()
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
interface ItemList {
id: number
name: string
description?: string
icon: string
iconColor?: string
pathName: string
display?: boolean
const chat = ref<Chat | null>(null)
const chatId = computed(() => parseIntString(route.params.chatId))
function initChat() {
if (chatsStore.isInit && chatId.value) {
const foundChat = chatsStore.chatById(chatId.value)
if (foundChat) {
chat.value = { ...foundChat } as Chat
}
}
}
const items = computed(() => ([
{ id: 1, name: 'account__subscribe', description: 'account__subscribe_description', icon: 'mdi-crown-circle-outline', iconColor: 'orange', pathName: 'subscribe' },
{ id: 2, name: 'account__auth_change_method', description: 'account__auth_change_method_description', icon: 'mdi-account-sync-outline', iconColor: 'primary', pathName: 'change_account_auth_method', display: !authStore.customer?.email },
{ id: 3, name: 'account__auth_change_password', description: 'account__auth_change_password_description', icon: 'mdi-account-key-outline', iconColor: 'primary', pathName: 'change_account_password', display: !!authStore.customer?.email },
{ id: 4, name: 'account__auth_change_account', description: 'account__auth_change_account_description', icon: 'mdi-account-switch-outline', iconColor: 'primary', pathName: 'change_account_email', display: !!authStore.customer?.email },
{ id: 5, name: 'account__company_data', icon: 'mdi-account-group-outline', description: 'account__company_data_description', pathName: 'your_company' },
{ id: 6, name: 'account__settings', icon: 'mdi-cog-outline', description: 'account__settings_description', iconColor: 'info', pathName: 'settings' },
{ id: 7, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
{ id: 9, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
{ id: 10, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' }
]))
if (chatsStore.isInit) initChat()
const displayItems = computed(() => (
items.value.filter((item: ItemList) => !('display' in item) || item.display === true)
))
watch(() => chatsStore.isInit, initChat)
async function goTo (path: string) {
await router.push({ name: path })
function onSubmit () {
if (chat.value && chat.value.invite_link) tg.openTelegramLink(chat.value.invite_link)
if (chat && chat.value) chatsStore.getChatUsers(chat.value.id)
}
async function logout () {
await authStore.logout()
await router.push({ name: 'login' })
const users = usersStore.getUsers
const companiesStore = useCompaniesStore()
const { userSection } = useUserSection()
const chatUsers = computed(() => {
if (!chat || !chat.value) return []
const idSet = new Set(chat.value.chat_users)
const arr = users.filter(el => idSet.has(el.id))
return arr.map(el => ({
...el,
...userSection(el),
companyName: el.company_id && companiesStore.companyById(el.company_id)
? companiesStore.companyById(el.company_id)?.name
: null
}))
})
async function goUserInfo (id: number) {
await router.push({ name: 'user_info', params: { id: route.params.id, userId: id }})
}
</script>
<style lang="scss">
</style>

View File

@@ -59,6 +59,7 @@
caption="settings__bot_title"
icon="mdi-map-clock-outline"
iconColor="primary"
v-if="timeZoneBot"
>
<template #value>
{{ timeZoneBot.tz }}
@@ -106,7 +107,7 @@
await settingsStore.updateSettings({ fontSize: newValue })
})
const timeZoneBot = ref<{ tz: string, offset: number }>({ tz: '', offset: 1 })
const timeZoneBot = ref<{ tz: string, offset: number,offsetString: string }>()
watch(timeZoneBot, async (newValue) => {
if (newValue) await settingsStore.updateSettings({ timeZoneBot: newValue })

View File

@@ -26,7 +26,7 @@
:key="item.id"
@right="handleSlide($event, item.id)"
right-color="red"
@click="goChat(item.invite_link)"
@click="goChatInfo(item.id)"
>
<template #right>
<q-icon size="lg" name="mdi-link-off"/>
@@ -169,6 +169,11 @@
import { useUsersStore } from 'stores/users'
import type { WebApp } from '@twa-dev/types'
import { useI18n } from "vue-i18n"
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const search = ref('')
@@ -259,8 +264,8 @@
tg.openTelegramLink(tgShareUrl)
}
function goChat (invite: string) {
tg.openTelegramLink(invite)
async function goChatInfo (chatId: number) {
await router.push({ name: 'chat_info', params: { id: route.params.id, chatId }})
}
// fix fab jumping
@@ -273,7 +278,6 @@
}, 300)
})
onDeactivated(() => {
showFab.value = false
if (timerId.value) {

View File

@@ -238,7 +238,7 @@
import { useRouter, useRoute } from 'vue-router'
import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies'
import type { User } from 'types/Users'
import { useUserSection } from 'composables/useUserSection'
defineOptions({ inheritAttrs: false })
const router = useRouter()
@@ -255,6 +255,7 @@
const showDialogBlockUser = ref<boolean>(false)
const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false)
const { userSection } = useUserSection()
const mapUsers = computed(() => users.map(el => ({
...el,
@@ -284,35 +285,6 @@
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_blocked))
function userSection (user: User) {
const tname = () => {
return user.firstname
? user.lastname
? user.firstname + ' ' + user.lastname
: user.firstname
: user.lastname ?? ''
}
const section1 = user.fullname ?? tname()
const section2_1 = user.fullname ? tname() : ''
const section2_2 = user.username ?? ''
const section3 = (
user.company_id && companiesStore.companyById(user.company_id)
? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department ) ? ' / ' :'')
: '') +
(user.department ? user.department + (user.role ? ' / ' : '') : '') +
(user.role ?? '')
return {
section1,
section2_1, section2_2,
section3
}
}
async function goUserInfo (id: number) {
await router.push({ name: 'user_info', params: { id: route.params.id, userId: id }})
}

View File

@@ -75,6 +75,12 @@ const routes: RouteRecordRaw[] = [
}
]
},
{
name: 'chat_info',
path: '/project/:id(\\d+)/chat/:chatId',
component: () => import('pages/ChatPage.vue'),
meta: { requiresAuth: true }
},
{
name: 'add_company',
path: '/project/:id(\\d+)/add-company',

View File

@@ -26,18 +26,23 @@ export const useChatsStore = defineStore('chats', () => {
}
async function unlink (chatId: number) {
const response = await api.get('/project/' + currentProjectId.value + '/chat/' + chatId)
const chatAPIid = response.data.data.id
const { data } = await api.get('/project/' + currentProjectId.value + '/chat/' + chatId)
const chatAPIid = data.data.id
const idx = chats.value.findIndex(item => item.id === chatAPIid)
chats.value.splice(idx, 1)
}
async function getKey () {
const response = await api.get('/project/' + currentProjectId.value + '/token')
const key = <string>response.data.data
const { data } = await api.get('/project/' + currentProjectId.value + '/token')
const key = <string>data.data
return key
}
async function getChatUsers (chatId: number) {
const { data } = await api.get('/project/' + currentProjectId.value + '/chat/' + chatId)
console.log(222, data)
}
const getChats = computed(() => chats.value)
function chatById (id: number) {
@@ -58,6 +63,7 @@ export const useChatsStore = defineStore('chats', () => {
unlink,
getKey,
getChats,
chatById
chatById,
getChatUsers
}
})

View File

@@ -80,11 +80,12 @@ export const useSettingsStore = defineStore('settings', () => {
if (authStore.isAuth) {
try {
const { data } = await api.get('/customer/settings')
console.log(data.data)
settings.value = {
fontSize: data.data.settings.fontSize || defaultSettings.fontSize,
locale: data.data.settings.locale || detectLocale(),
timeZoneBot: data.data.settings.timeZone || defaultSettings.timeZoneBot,
localeBot: data.data.settings.localeBot || detectLocale()
fontSize: data.data.fontSize || defaultSettings.fontSize,
locale: data.data.locale || detectLocale(),
timeZoneBot: data.data.timeZone || defaultSettings.timeZoneBot,
localeBot: data.data.localeBot || detectLocale()
}
} catch {
settings.value.locale = detectLocale()
@@ -101,7 +102,7 @@ export const useSettingsStore = defineStore('settings', () => {
}
const saveSettings = async () => {
await api.put('/customer/settings', { settings: settings.value })
await api.put('/customer/settings', settings.value)
}
const updateSettings = async (newSettings: Partial<AppSettings>) => {

View File

@@ -11,7 +11,8 @@ interface Chat {
logo: string | null
owner_id?: number
invite_link: string
[key: string]: unknown
chat_users: number []
[key: string]: number | string | boolean | null | number[]
}
export type {