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

View File

@@ -98,6 +98,28 @@
class = "w100 q-pt-sm" class = "w100 q-pt-sm"
:label = "$t('user_block__role')" :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>
</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> <template>
<pn-page-card> <pn-page-card>
<template #title> <template #title>
<pn-account-block-name/> {{ $t('chat_card__title') }}
</template>
<template #footer>
<q-btn <q-btn
@click="logout()" rounded color="primary"
flat class="w100 q-mt-md q-mb-xs"
round @click = "onSubmit"
icon="mdi-logout" v-if="chat && chat.invite_link"
/> >
{{ $t("chat_card__go_chat") }}
</q-btn>
</template> </template>
<pn-scroll-list> <pn-scroll-list>
<q-list separator> <div
<q-item v-if="chat"
v-for="item in displayItems" class="flex column items-center q-pa-md q-pb-lg"
:key="item.id" >
@click="goTo(item.pathName)" <pn-auto-avatar
clickable :img="chat.logo"
v-ripple :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> {{ chat.description }}
<q-avatar </div>
:icon="item.icon" </div>
:color="item.iconColor ? item.iconColor: 'brand'"
text-color="white" <q-list separator v-if="chatUsers.length!==0">
rounded <q-item-label header>
font-size ="26px" {{ $t('chat_page__members') + ' (' + chatUsers.length +')' }}
/> </q-item-label>
</q-item-section> <q-item
<q-item-section> v-for="item in chatUsers"
<q-item-label> :key="item.id"
{{ $t(item.name) }} v-ripple
</q-item-label> clickable
<q-item-label class="text-caption" v-if="$te(item.description)"> class="w100"
{{ $t(item.description) }} @click="goUserInfo(item.id)"
</q-item-label> >
</q-item-section> <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-item>
</q-list> </q-list>
</pn-scroll-list> </pn-scroll-list>
</pn-page-card> </pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { ref, computed, watch, inject } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from 'stores/auth' 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 router = useRouter()
const authStore = useAuthStore() const route = useRoute()
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
interface ItemList { const chat = ref<Chat | null>(null)
id: number const chatId = computed(() => parseIntString(route.params.chatId))
name: string
description?: string function initChat() {
icon: string if (chatsStore.isInit && chatId.value) {
iconColor?: string const foundChat = chatsStore.chatById(chatId.value)
pathName: string if (foundChat) {
display?: boolean chat.value = { ...foundChat } as Chat
}
}
} }
const items = computed(() => ([ if (chatsStore.isInit) initChat()
{ 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' }
]))
const displayItems = computed(() => ( watch(() => chatsStore.isInit, initChat)
items.value.filter((item: ItemList) => !('display' in item) || item.display === true)
))
async function goTo (path: string) { function onSubmit () {
await router.push({ name: path }) 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 () { const users = usersStore.getUsers
await authStore.logout() const companiesStore = useCompaniesStore()
await router.push({ name: 'login' }) 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> </script>
<style lang="scss">
</style>

View File

@@ -59,6 +59,7 @@
caption="settings__bot_title" caption="settings__bot_title"
icon="mdi-map-clock-outline" icon="mdi-map-clock-outline"
iconColor="primary" iconColor="primary"
v-if="timeZoneBot"
> >
<template #value> <template #value>
{{ timeZoneBot.tz }} {{ timeZoneBot.tz }}
@@ -106,7 +107,7 @@
await settingsStore.updateSettings({ fontSize: newValue }) 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) => { watch(timeZoneBot, async (newValue) => {
if (newValue) await settingsStore.updateSettings({ timeZoneBot: newValue }) if (newValue) await settingsStore.updateSettings({ timeZoneBot: newValue })

View File

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

View File

@@ -238,7 +238,7 @@
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useUsersStore } from 'stores/users' import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import type { User } from 'types/Users' import { useUserSection } from 'composables/useUserSection'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
const router = useRouter() const router = useRouter()
@@ -255,6 +255,7 @@
const showDialogBlockUser = ref<boolean>(false) const showDialogBlockUser = ref<boolean>(false)
const currentSlideEvent = ref<SlideEvent | null>(null) const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false) const closedByUserAction = ref(false)
const { userSection } = useUserSection()
const mapUsers = computed(() => users.map(el => ({ const mapUsers = computed(() => users.map(el => ({
...el, ...el,
@@ -284,35 +285,6 @@
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_blocked)) 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) { async function goUserInfo (id: number) {
await router.push({ name: 'user_info', params: { id: route.params.id, userId: id }}) 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', name: 'add_company',
path: '/project/:id(\\d+)/add-company', path: '/project/:id(\\d+)/add-company',

View File

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

View File

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

View File

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