This commit is contained in:
2025-06-05 20:00:58 +03:00
parent c8f3c9801f
commit 1c732e16dd
203 changed files with 9793 additions and 3960 deletions

View File

@@ -1,17 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('account__change_password')}}
</div>
</template>
<pn-scroll-list>
<account-helper :type />
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import accountHelper from 'components/admin/accountHelper.vue'
const type = 'change'
</script>

View File

@@ -1,17 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('account__change_password')}}
</div>
</template>
<pn-scroll-list>
<account-helper :type />
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import accountHelper from 'components/admin/accountHelper.vue'
const type = 'change'
</script>

View File

@@ -1,17 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('login__register')}}
</div>
</template>
<pn-scroll-list>
<account-helper :type />
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import accountHelper from 'components/admin/accountHelper.vue'
const type = 'new'
</script>

View File

@@ -1,22 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('forgot_password__password_recovery')}}
</div>
</template>
<pn-scroll-list>
<account-helper :type :email="email"/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from 'vue-router' // Добавляем импорт
import accountHelper from 'components/admin/accountHelper.vue'
const route = useRoute()
const type = 'forgot'
const email = ref(route.query.email as string)
</script>

View File

@@ -1,88 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex justify-between items-center text-white q-pa-sm w100">
<div class="flex items-center justify-center row">
<q-avatar v-if="tgUser?.photo_url" size="48px" class="q-mr-xs">
<q-img :src="tgUser.photo_url"/>
</q-avatar>
<div class="flex column">
<span class="q-ml-xs text-h5">
{{
tgUser?.first_name +
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
tgUser?.last_name
}}
</span>
<span class="q-ml-xs text-caption">
{{ tgUser?.username }}
</span>
</div>
</div>
<q-btn flat icon="mdi-logout"/>
</div>
</template>
<pn-scroll-list>
<q-list separator>
<q-item
v-for="item in items"
:key="item.id"
@click="goTo(item.pathName)"
clickable
v-ripple
>
<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">
{{ $t(item.description) }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { inject, computed } from 'vue'
import { useRouter } from 'vue-router'
// import { useAuthStore } from 'stores/auth'
import type { WebApp } from '@twa-dev/types'
const router = useRouter()
// const authStore = useAuthStore()
const tg = inject('tg') as WebApp
const tgUser = tg.initDataUnsafe.user
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: '' },
{ id: 3, name: 'account__auth_change_password', description: 'account__auth_change_password_description', icon: 'mdi-account-key-outline', iconColor: 'primary', pathName: 'change_account_password' },
{ id: 4, name: 'account__auth_change_account', description: 'account__auth_change_account_description', icon: 'mdi-account-switch-outline', iconColor: 'primary', pathName: 'change_account_email' },
{ id: 5, name: 'account__company_data', icon: 'mdi-account-group-outline', description: 'account__company_data_description', pathName: 'your_company' },
{ id: 6, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
{ id: 7, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
{ id: 8, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' }
]))
async function goTo (path: string) {
await router.push({ name: path })
}
</script>
<style lang="scss">
</style>

View File

@@ -1,39 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('company_create__title_card')}}
</div>
<q-btn
v-if="(Object.keys(companyMod).length !== 0)"
@click = "addCompany(companyMod)"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<company-info-block v-model="companyMod"/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import companyInfoBlock from 'components/admin/companyInfoBlock.vue'
import { useCompaniesStore } from 'stores/companies'
import type { Company } from 'src/types'
const router = useRouter()
const companiesStore = useCompaniesStore()
const companyMod = ref(<Company>{})
function addCompany (data: Company) {
companiesStore.addCompany(data)
router.go(-1)
}
</script>

View File

@@ -1,74 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('company_info__title_card')}}
</div>
<q-btn
v-if="isFormValid && isDirty()"
@click = "updateCompany()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<company-info-block
v-if="company"
v-model="company"
@valid="isFormValid = $event"
/>
<company-info-persons/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import companyInfoBlock from 'components/admin/companyInfoBlock.vue'
import companyInfoPersons from 'components/admin/companyInfoPersons.vue'
import { useCompaniesStore } from 'stores/companies'
import type { Company } from 'src/types'
import { parseIntString, isObjEqual } from 'boot/helpers'
const router = useRouter()
const route = useRoute()
const companiesStore = useCompaniesStore()
const company = ref<Company>()
const companyId = parseIntString(route.params.companyId)
const isFormValid = ref(false)
const originalCompany = ref<Company>({} as Company)
const isDirty = () => {
return company.value && !isObjEqual(originalCompany.value, company.value)
}
onMounted(async () => {
if (companyId && companiesStore.companyById(companyId)) {
const initial = companiesStore.companyById(companyId)
company.value = { ...initial } as Company
originalCompany.value = JSON.parse(JSON.stringify(company.value))
} else {
await abort()
}
})
function updateCompany () {
if (companyId && company.value) {
companiesStore.updateCompany(companyId, company.value)
router.back()
}
}
async function abort () {
await router.replace({name: 'projects'})
}
</script>

View File

@@ -1,157 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('company__mask')}}
</div>
</template>
<pn-scroll-list>
<template #card-body-header>
<div style="min-height: var(--top-raduis);"/>
</template>
<q-list
separator
>
<q-item>
<q-item-section>
<div>
<q-btn flat round color="primary" icon="mdi-help-circle-outline" @click="showDialogHelp = true" />
</div>
</q-item-section>
<q-item-section></q-item-section>
<q-item-section align="end" class="col-grow">
{{ $t('mask__title_table') }}
</q-item-section>
</q-item>
<q-item
v-for = "company in companies"
:key="company.id"
class="w100"
>
<q-item-section>
<q-checkbox
v-model="company.masked"
:label="company.name"
unchecked-icon="mdi-drama-masks"
checked-icon="mdi-drama-masks"
:class="company.masked ? 'masked' : 'unmasked'"
/>
</q-item-section>
<q-item-section>
<q-select
v-if="company.masked"
v-model="company.unmasked"
multiple
:options="companiesSelect(company.id)"
dense
borderless
dropdown-icon="mdi-plus"
class="fix-select"
>
<template #selected>
<div
v-for="(comp, idx) in company.unmasked"
:key=idx
class="q-pa-xs"
>
<q-avatar rounded size="md">
<img v-if="comp['logo']" :src="comp['logo']"/>
<pn-auto-avatar v-else :name="comp['name']"/>
</q-avatar>
</div>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</q-item-section>
</q-item>
<q-item
class="q-pa-none q-ma-none"
style="min-height: 18px"
>
<div class="q-py-none flex column w100"/>
</q-item>
</q-list>
</pn-scroll-list>
<q-dialog v-model="showDialogHelp">
<q-card class="q-ma-sm w100">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6">
<q-icon name="mdi-drama-masks"/>
{{ $t('mask__help_title')}}
</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="q-pt-sm">
{{ $t('mask__help_message')}}
</q-card-section>
</q-card>
</q-dialog>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// import { useCompaniesStore } from 'src/stores/companies'
const showDialogHelp = ref<boolean>(false)
// const companiesStore = useCompaniesStore()
// const companies = computed(() => companiesStore.companies)
const companies = ref([
{id: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
function companiesSelect (id :string) {
return companies.value
.map(el => ({
id: el.id,
name: el.name,
logo: el.logo
}))
.filter(el => el.id !== id)
}
</script>
<style scoped lang="scss">
:deep(.fix-select .q-field__control) {
align-items: center;
}
:deep(.fix-select .q-icon) {
color: $green-14 !important;
}
:deep(.masked .q-icon) {
color: red;
opacity: 1;
}
:deep(.unmasked .q-icon) {
color: grey;
opacity: 0.5;
}
</style>

View File

@@ -1,39 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('Your_company__title_card')}}
</div>
<q-btn
v-if="(Object.keys(companyMod).length !== 0)"
@click = "addCompany(companyMod)"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<company-info-block v-model="companyMod"/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import companyInfoBlock from 'components/admin/companyInfoBlock.vue'
import { useCompaniesStore } from 'stores/companies'
import type { Company } from 'src/types'
const router = useRouter()
const companiesStore = useCompaniesStore()
const companyMod = ref(<Company>{})
function addCompany (data: Company) {
companiesStore.addCompany(data)
router.go(-1)
}
</script>

View File

@@ -1,215 +0,0 @@
<template>
<q-page class="flex column items-center justify-between">
<q-card
id="login_block"
flat
class="flex column items-center w80 justify-between q-py-lg login-card "
>
<login-logo
class="col-grow q-pa-md"
:style="{ alignItems: 'flex-end' }"
/>
<div class = "q-ma-md flex column input-login">
<q-input
v-model="login"
dense
filled
class = "q-mb-md"
:label = "$t('login__email')"
/>
<q-input
v-model="password"
dense
filled
:label = "$t('login__password')"
class = "q-mb-md"
:type="isPwd ? 'password' : 'text'"
>
<template #append>
<q-icon
color="grey-5"
:name="isPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<div class="self-end">
<q-btn
@click="forgotPwd"
flat
no-caps
dense
class="text-grey"
>
{{$t('login__forgot_password')}}
</q-btn>
</div>
</div>
<q-btn
@click="sendAuth"
color="primary"
:disabled="!acceptTermsOfUse"
>
{{$t('login__sign_in')}}
</q-btn>
<div class="q-pt-lg">
<q-btn
flat
sm
no-caps
color="primary"
@click="createAccount()"
>
{{$t('login__register')}}
</q-btn>
</div>
<div
v-if="isTelegramApp"
id="alt_login"
class="w80 q-flex column items-center q-pt-xl"
>
<div
class="orline w100 text-grey"
>
<span class="q-mx-sm">{{$t('login__or_continue_as')}}</span>
</div>
<q-btn
flat
sm
no-caps
color="primary"
:disabled="!acceptTermsOfUse"
@click="handleTelegramLogin"
>
<div class="flex items-center text-blue">
<q-icon name="telegram" size="md" class="q-mx-none text-blue"/>
<div class="q-ml-xs ellipsis" style="max-width: 100px">
{{
tgUser?.first_name +
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
tgUser?.last_name
}}
</div>
</div>
</q-btn>
</div>
</q-card>
<div id="term-of-use" class="q-py-lg text-white q-flex row">
<q-checkbox
v-model="acceptTermsOfUse"
checked-icon="task_alt"
unchecked-icon="highlight_off"
:color="acceptTermsOfUse ? 'brand' : 'red'"
dense
keep-color
/>
<span class="q-px-xs">
{{$t('login__accept_terms_of_use') + ' '}}
</span>
<span class="text-cyan-12">
{{$t('login__terms_of_use') }}
</span>
</div>
</q-page>
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue'
import { useQuasar } from 'quasar'
import { useRouter } from 'vue-router'
import loginLogo from 'components/admin/login-page/loginLogo.vue'
import { useI18n } from "vue-i18n"
import { useAuthStore } from 'src/stores/auth'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const tgUser = tg.initDataUnsafe.user
const authStore = useAuthStore()
const router = useRouter()
const $q = useQuasar()
const { t } = useI18n()
const login = ref<string>('')
const password = ref<string>('')
const isPwd = ref<boolean>(true)
const acceptTermsOfUse = ref<boolean>(true)
function onErrorLogin () {
$q.notify({
message: t('login__incorrect_login_data'),
type: 'negative',
position: 'bottom',
timeout: 2000,
multiLine: true
})
}
async function sendAuth() {
console.log('1')
await router.push({ name: 'projects' })
}
async function forgotPwd() {
await router.push({
name: 'recovery_password',
query: { email: login.value }
})
}
async function createAccount() {
await router.push({ name: 'create_account' })
}
const isTelegramApp = computed(() => {
// @ts-expect-ignore
return !!window.Telegram?.WebApp?.initData
})
/* const handleSubmit = async () => {
await authStore.loginWithCredentials(email.value, password.value)
} */
async function handleTelegramLogin () {
// @ts-expect-ignore
const initData = window.Telegram.WebApp.initData
await authStore.loginWithTelegram(initData)
}
</script>
<style scoped>
.maxh15 {
max-height: calc(100Vh *0.15);
}
.input-login {
width: calc(100% * 0.8);
max-width: 300px;
}
.orline {
display: flex;
flex-direction: row;
}
.orline:before,
.orline:after {
content: "";
flex: 1 1;
border-bottom: 1px solid;
margin: auto;
}
.login-card {
opacity: 0.9 !important;
border-radius: var(--top-raduis);
}
</style>

View File

@@ -6,10 +6,7 @@
<q-tab-panels
v-model="tabSelect"
animated
keep-alive
@before-transition="showFab = false"
@transition="showFab = true"
class="tab-panel-color full-height-panel w100 flex column col-grow no-scroll"
>
<q-tab-panel
@@ -19,12 +16,11 @@
class="q-pa-none flex column col-grow no-scroll"
style="flex-grow: 2"
>
<component :is="tab.component" :showFab = "tab.name === tabSelect" />
<router-view/>
</q-tab-panel>
</q-tab-panels>
<template #footer>
<q-footer class="bg-grey-1 text-grey">
<q-footer class="bg-grey-1 text-grey main-content">
<q-tabs
style = "z-index: 1000"
v-model="tabSelect"
@@ -40,11 +36,11 @@
no-caps
dense
:class="tabSelect === tab.name ? 'active' : ''"
class="w100 flex column"
class="flex column w100"
>
<template #default>
<div class="flex column items-center">
<q-icon :name="tab.icon" size="sm">
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'">
<q-badge
color="brand" align="top"
rounded floating
@@ -53,10 +49,20 @@
{{ currentProject?.[tab.name as keyof typeof currentProject] ?? 0 }}
</q-badge>
</q-icon>
<span class="text-caption">{{$t(tab.label)}}</span>
<div
class="text-caption flex justify-center"
:style="{ width: (baseTabWidth - 32) + 'px'}"
v-if="maxTabWidth < baseTabWidth"
>
<span>
<q-resize-observer @resize="size => tab.width = size.width"/>
{{$t(tab.label)}}
</span>
</div>
</div>
</template>
</q-route-tab>
<q-resize-observer @resize="size => tabsWidth = size.width" />
</q-tabs>
</q-footer>
</template>
@@ -66,35 +72,31 @@
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { useProjectsStore } from 'stores/projects'
import projectPageHeader from 'components/admin/project-page/ProjectPageHeader.vue'
import projectPageChats from 'components/admin/project-page/ProjectPageChats.vue'
import projectPageCompanies from 'components/admin/project-page/ProjectPageCompanies.vue'
import projectPagePersons from 'components/admin/project-page/ProjectPagePersons.vue'
import { useRoute } from 'vue-router'
import projectPageHeader from 'pages/main/HeaderPage.vue'
const route = useRoute()
const showFab = ref<boolean>(false)
const projectsStore = useProjectsStore()
const currentProject = computed(() => projectsStore.currentProjectId)
const projectStore = useProjectsStore()
const currentProject = computed(() => projectStore.getCurrentProject() )
const tabs = ref([
{name: 'files', label: 'main__files', icon: 'mdi-file-multiple-outline', to: { name: 'files'}, width: 0 },
{name: 'tasks', label: 'main__tasks', icon: 'mdi-clipboard-outline', to: { name: 'tasks'}, width: 0 },
{name: 'meetings', label: 'main__meetings', icon: 'mdi-calendar-month-outline', to: { name: 'meetings'}, width: 0 },
{name: 'users', label: 'main__users', icon: 'mdi-account-outline', to: { name: 'users'}, width: 0 },
{name: 'chats', label: 'main__chats', icon: 'mdi-chat-outline', to: { name: 'chats'}, width: 0 }
])
const tabComponents = {
projectPageChats,
projectPagePersons,
projectPageCompanies
}
const tabs = [
{name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline', component: tabComponents.projectPageChats, to: { name: 'chats'} },
{name: 'persons', label: 'project__persons', icon: 'mdi-account-outline', component: tabComponents.projectPagePersons, to: { name: 'persons'} },
{name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline', component: tabComponents.projectPageCompanies, to: { name: 'companies'} },
]
// hidden icon name if overflow - with resize-observer
const tabsWidth = ref(0)
const baseTabWidth = computed(() => Math.floor(tabsWidth.value / 5))
const maxTabWidth = computed(() => Math.max(...tabs.value.map(el => el.width)))
const tabSelect = ref<string>()
onBeforeMount(() => {
const initialTab = tabs.find(t => t.to.name === route.name)?.name || tabs[0]?.name || ''
const initialTab = tabs.value.find(t => t.to.name === route.name)?.name || tabs.value[0]?.name || ''
tabSelect.value = initialTab
})
@@ -122,4 +124,8 @@
#tabs :deep(.q-tab__indicator) {
height: 3px !important;
}
#tabs :deep(.q-tabs__arrow) {
display: none;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<pn-page-card>
<template #title>
{{$t('meeting_create__title_card')}}
<q-btn
v-if="(Object.keys(meetingMod).length !== 0)"
@click = "addCompany(meetingMod)"
flat round
icon="mdi-check"
/>
</template>
<pn-scroll-list>
<meeting-block v-model="meetingMod"/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import meetingBlock from 'components/meetingBlock.vue'
import { useMeetingsStore } from 'stores/meetings'
import type { MeetingParams } from 'types/Meeting'
const router = useRouter()
const meetingsStore = useMeetingsStore()
const meetingMod = ref(<MeetingParams>{
name: '',
description: '',
place: '',
meet_date: Date.now(),
chat_attach: null,
participants: [],
files: [],
is_cancel: false
})
async function addCompany (data: MeetingParams) {
await meetingsStore.add(data)
router.go(-1)
}
</script>

View File

@@ -0,0 +1,236 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('meeting_add__title') }}
</div>
<q-btn
@click = "createMeeting()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<div class="flex column items-center q-ma-lg">
<div class="q-gutter-y-lg w100">
<q-input
v-model.trim="task.name"
dense
filled
autogrow
:label = "$t('task_add__name')"
class="bold-input w100"
/>
<q-input
v-model.trim="task.description"
dense
filled
autogrow
class = "w100"
:label = "$t('task_add__description')"
/>
<q-file
v-model="task.files"
:label="$t('task_add__attach_files')"
outlined
use-chips
multiple
dense
class="file-input-fix"
>
<template #append>
<q-icon name="attach_file"/>
</template>
</q-file>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__assigned_to')"
>
<template #prepend>
<q-icon name="mdi-account-arrow-left-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__watch')"
>
<template #prepend>
<q-icon name="mdi-account-eye-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
<q-input filled v-model="task.date" dense :label="$t('task_add__plan_date')">
<template #prepend>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="task.date" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
<template #append>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-time v-model="task.date" mask="YYYY-MM-DD HH:mm" format24h>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<div class="flex column">
<q-item-label header class="q-py-none q-my-none">
{{$t('task_add__priority')}}
</q-item-label>
<q-btn-toggle
v-model="task.priority"
no-caps
toggle-color="primary"
spread
unelevated
:options="priority"
class="q-mt-sm w100"
>
<template v-for="item in priority" :key="item.id" #[item.slot]>
<div class="row items-center no-wrap gap-xs text-weight-regular">
<span>
{{ $t(item.translationKey) }}
</span>
<pn-task-priority-icon :priority="item.value"/>
</div>
</template>
</q-btn-toggle>
</div>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('meeting_add__attached_chat')"
>
<template #prepend>
<q-icon name="mdi-chat-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const task = ref({id: "p1", name: 'Кирюшкин Андрей', description: 'fdsfdsfsdfs', company: '', priority: 1, department: 'test', files: [], date: new Date(Date.now()).toLocaleString() })
const priority = [
{ id: 1, slot: 's1', label: '', translationKey: 'task_add__priority_normal', value: 0 },
{ id: 2, slot: 's2', label: '', translationKey: 'task_add__priority_important', value: 1 },
{ id: 3, slot: 's3', label: '', translationKey: 'task_add__priority_critical', value: 2 }
]
const companies = ref([
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
async function createMeeting () {
await router.push({ name: 'meetings' })
}
</script>
<style scoped>
.bold-input::v-deep .q-field__native {
font-weight: bold;
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('meeting_add__title') }}
</div>
<q-btn
@click = "createMeeting()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<div class="flex column items-center q-ma-lg">
<div class="q-gutter-y-lg w100">
<q-input
v-model.trim="task.name"
dense
filled
autogrow
:label = "$t('task_add__name')"
class="bold-input w100"
/>
<q-input
v-model.trim="task.description"
dense
filled
autogrow
class = "w100"
:label = "$t('task_add__description')"
/>
<q-file
v-model="task.files"
:label="$t('task_add__attach_files')"
outlined
use-chips
multiple
dense
class="file-input-fix"
>
<template #append>
<q-icon name="attach_file"/>
</template>
</q-file>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__assigned_to')"
>
<template #prepend>
<q-icon name="mdi-account-arrow-left-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__watch')"
>
<template #prepend>
<q-icon name="mdi-account-eye-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
<q-input filled v-model="task.date" dense :label="$t('task_add__plan_date')">
<template #prepend>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="task.date" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
<template #append>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-time v-model="task.date" mask="YYYY-MM-DD HH:mm" format24h>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<div class="flex column">
<q-item-label header class="q-py-none q-my-none">
{{$t('task_add__priority')}}
</q-item-label>
<q-btn-toggle
v-model="task.priority"
no-caps
toggle-color="primary"
spread
unelevated
:options="priority"
class="q-mt-sm w100"
>
<template v-for="item in priority" :key="item.id" #[item.slot]>
<div class="row items-center no-wrap gap-xs text-weight-regular">
<span>
{{ $t(item.translationKey) }}
</span>
<pn-task-priority-icon :priority="item.value"/>
</div>
</template>
</q-btn-toggle>
</div>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('meeting_add__attached_chat')"
>
<template #prepend>
<q-icon name="mdi-chat-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const task = ref({id: "p1", name: 'Кирюшкин Андрей', description: 'fdsfdsfsdfs', company: '', priority: 1, department: 'test', files: [], date: new Date(Date.now()).toLocaleString() })
const priority = [
{ id: 1, slot: 's1', label: '', translationKey: 'task_add__priority_normal', value: 0 },
{ id: 2, slot: 's2', label: '', translationKey: 'task_add__priority_important', value: 1 },
{ id: 3, slot: 's3', label: '', translationKey: 'task_add__priority_critical', value: 2 }
]
const companies = ref([
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
async function createMeeting () {
await router.push({ name: 'meetings' })
}
</script>
<style scoped>
.bold-input::v-deep .q-field__native {
font-weight: bold;
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
</style>

View File

@@ -1,113 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('person_card__title') }}
</div>
<q-btn
@click = "goProject()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<div class="flex column items-center q-ma-lg">
<q-avatar size="100px">
<q-img :src="person.logo"/>
</q-avatar>
<div class="flex row items-start justify-center no-wrap q-pb-lg">
<div class="flex column justify-center">
<div class="text-bold q-pr-xs text-center">{{ person.tname }}</div>
<div caption class="text-blue text-caption">{{ person.tusername }}</div>
</div>
</div>
<div class="q-gutter-y-lg w100">
<q-input
v-model="person.name"
dense
filled
class = "w100"
:label = "$t('person_card__name')"
/>
<q-select
v-if="companies"
v-model="person.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('person_card__company')"
>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(person.company)).name }}
</template>
</q-select>
<q-input
v-model="person.department"
dense
filled
class = "w100"
:label = "$t('person_card__department')"
/>
<q-input
v-model="person.role"
dense
filled
class = "w100"
:label = "$t('person_card__role')"
/>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const person = ref({id: "p1", name: 'Кирюшкин Андрей', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', tname: 'Kir_AA', tusername: '@kiruha90', role: 'DevOps', company: '', department: 'test' })
const companies = ref([
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
async function goProject () {
await router.push({ name: 'project' })
}
</script>
<style>
</style>

View File

@@ -1,22 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('privacy__title') }}
</div>
</div>
</template>
<pn-scroll-list>
{{ $t('under_construction') }}
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
</script>
<style>
</style>

View File

@@ -1,60 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('project_card__add_project')}}
</div>
<q-btn
v-if="isFormValid && isDirty"
@click = "addProject(project)"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<project-info-block
v-model="project"
@valid="isFormValid = $event"
/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import projectInfoBlock from 'components/admin/projectInfoBlock.vue'
import { useProjectsStore } from 'stores/projects'
import type { ProjectParams } from 'src/types'
const router = useRouter()
const projectsStore = useProjectsStore()
const initialProject: ProjectParams = {
name: '',
logo: '',
description: '',
logo_as_bg: false
}
const project = ref<ProjectParams>({ ...initialProject })
const isFormValid = ref(false)
const isDirty = computed(() => {
return (
project.value.name !== initialProject.name ||
project.value.logo !== initialProject.logo ||
project.value.description !== initialProject.description ||
project.value.logo_as_bg !== initialProject.logo_as_bg
)
})
async function addProject (data: ProjectParams) {
const newProject = projectsStore.addProject(data)
await router.push({name: 'chats', params: { id: newProject.id}})
}
</script>

View File

@@ -1,75 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
<span>{{ $t('project_card__project_card') }}</span>
</div>
<q-btn
v-if="isFormValid && isDirty()"
@click="updateProject()"
flat
round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<project-info-block
v-if="project"
v-model="project"
@valid="isFormValid = $event"
/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useProjectsStore } from 'stores/projects'
import projectInfoBlock from 'components/admin/projectInfoBlock.vue'
import type { Project } from '../types'
import { parseIntString, isObjEqual } from 'boot/helpers'
const router = useRouter()
const route = useRoute()
const projectsStore = useProjectsStore()
const project = ref<Project>()
const id = parseIntString(route.params.id)
const isFormValid = ref(false)
const originalProject = ref<Project>({} as Project)
const isDirty = () => {
return project.value && !isObjEqual(originalProject.value, project.value)
}
onMounted(async () => {
if (id && projectsStore.projectById(id)) {
const initial = projectsStore.projectById(id)
project.value = { ...initial } as Project
originalProject.value = JSON.parse(JSON.stringify(project.value))
} else {
await abort()
}
})
function updateProject () {
if (id && project.value) {
projectsStore.updateProject(id, project.value)
router.back()
}
}
async function abort () {
await router.replace({name: 'projects'})
}
</script>
<style>
</style>

View File

@@ -1,233 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>{{ $t('projects__projects') }}</div>
<div class="flex items-center">
<q-btn
@click="goAccount()"
flat
no-caps
icon-right="mdi-chevron-right"
align="right"
dense
>
<div class="flex items-center">
<q-avatar v-if="tgUser?.photo_url" size="32px">
<q-img :src="tgUser.photo_url"/>
</q-avatar>
<div class="q-ml-xs ellipsis" style="max-width: 100px">
{{
tgUser?.first_name +
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
tgUser?.last_name
}}
</div>
</div>
</q-btn>
</div>
</div>
</template>
<pn-scroll-list>
<template #card-body-header>
<q-input
v-model="searchProject"
clearable
clear-icon="close"
:placeholder="$t('project_chats__search')"
dense
class="col-grow q-px-md q-py-md"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</template>
<q-list separator>
<q-item
v-for = "item in activeProjects"
:key="item.id"
clickable
v-ripple
@click="goProject(item.id)"
class="w100"
>
<q-item-section avatar>
<q-avatar rounded >
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="height: 40px"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label caption lines="2">{{item.description}}</q-item-label>
</q-item-section>
<q-item-section side class="text-caption ">
<div class="flex items-center column">
<div class="flex items-center">
<q-icon name="mdi-chat-outline"/>
<span>{{ item.chats }} </span>
</div>
<div class="flex items-center">
<q-icon name="mdi-account-outline"/>
<span>{{ item.persons }}</span>
</div>
</div>
</q-item-section>
</q-item>
</q-list>
<div v-if="archiveProjects.length !== 0" class="flex column items-center w100" :class="showArchive ? 'bg-grey-12' : ''">
<div id="btn_show_archive">
<q-btn-dropdown color="grey" flat no-caps @click="showArchive = !showArchive" dropdown-icon="arrow_drop_up">
<template #label>
<span class="text-caption">
<span v-if="!showArchive">{{ $t('projects__show_archive') }}</span>
<span v-else>{{ $t('projects__hide_archive') }}</span>
</span>
</template>
</q-btn-dropdown>
</div>
<q-list separator v-if="showArchive" class="w100">
<q-item
v-for = "item in archiveProjects"
:key="item.id"
clickable
v-ripple
@click="handleArchiveList(item.id)"
class="w100 text-grey"
>
<q-item-section avatar>
<q-avatar rounded >
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="height: 40px"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label caption lines="2">{{item.description}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</pn-scroll-list>
<q-page-sticky
position="bottom-right"
:offset="[18, 18]"
>
<q-btn
fab
icon="add"
color="brand"
@click="createNewProject"
/>
</q-page-sticky>
</pn-page-card>
<q-dialog v-model="showDialogArchive">
<q-card class="q-pa-none q-ma-none">
<q-card-section align="center">
<div class="text-h6 text-negative ">{{ $t('projects__restore_archive_warning') }}</div>
</q-card-section>
<q-card-section class="q-pt-none" align="center">
{{ $t('projects__restore_archive_warning_message') }}
</q-card-section>
<q-card-actions align="center">
<q-btn
flat
:label="$t('back')"
color="primary"
v-close-popup
/>
<q-btn
flat
:label="$t('continue')"
color="primary"
v-close-popup
@click="restoreFromArchive()"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, inject } from 'vue'
import { useRouter } from 'vue-router'
import { useProjectsStore } from 'stores/projects'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const tgUser = tg.initDataUnsafe.user
const router = useRouter()
const projectsStore = useProjectsStore()
const projects = projectsStore.projects
const searchProject = ref('')
const showArchive = ref(false)
const showDialogArchive = ref(false)
const archiveProjectId = ref<number | undefined> (undefined)
async function goProject (id: number) {
await router.push({ name: 'chats', params: { id }})
}
async function goAccount () {
await router.push({ name: 'account' })
}
async function createNewProject () {
await router.push({ name: 'project_add' })
}
function handleArchiveList (id: number) {
showDialogArchive.value = true
archiveProjectId.value = id
}
function restoreFromArchive () {
if (archiveProjectId.value) {
const projectTemp = projectsStore.projectById(archiveProjectId.value)
if (projectTemp) {
projectTemp.is_archive = false
projectsStore.updateProject(archiveProjectId.value, projectTemp)
}
}
}
const displayProjects = computed(() => {
if (!searchProject.value || !(searchProject.value && searchProject.value.trim())) return projects
const searchChatValue = searchProject.value.trim().toLowerCase()
const arrOut = projects
.filter(el =>
el.name.toLowerCase().includes(searchChatValue) ||
el.description && el.description.toLowerCase().includes(searchProject.value)
)
return arrOut
})
const activeProjects = computed(() => {
return displayProjects.value.filter(el => !el.is_archive)
})
const archiveProjects = computed(() => {
return displayProjects.value.filter(el => el.is_archive)
})
watch(showDialogArchive, (newD :boolean) => {
if (!newD) archiveProjectId.value = undefined
})
</script>
<style>
</style>

View File

@@ -1,11 +1,7 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('settings__title') }}
</div>
</div>
{{ $t('settings__title') }}
</template>
<pn-scroll-list>
@@ -40,18 +36,18 @@
<q-item-section>
<div class="flex justify-end">
<q-btn
@click="textSizeStore.decreaseFontSize()"
@click="settingsStore.decreaseFontSize()"
color="negative" flat
icon="mdi-format-font-size-decrease"
class="q-pa-sm q-mx-xs"
:disable="currentTextSize <= minTextSize"
:disable="!settingsStore.canDecrease"
/>
<q-btn
@click="textSizeStore.increaseFontSize()"
@click="settingsStore.increaseFontSize()"
color="positive" flat
icon="mdi-format-font-size-increase"
class="q-pa-sm q-mx-xs"
:disable="currentTextSize >= maxTextSize"
:disable="!settingsStore.canIncrease"
/>
</div>
</q-item-section>
@@ -62,32 +58,23 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { watch, ref } from 'vue'
import { useTextSizeStore } from 'src/stores/textSize'
import { computed } from 'vue'
import { useSettingsStore } from 'stores/settings'
const { locale } = useI18n()
const savedLocale = localStorage.getItem('locale') || 'en-US'
locale.value = savedLocale
const settingsStore = useSettingsStore()
const localeOptions = ref([
{ value: 'en-US', label: 'English' },
{ value: 'ru-RU', label: 'Русский' }
])
const localeOptions = settingsStore.supportLocale
watch(locale, (newLocale) => {
localStorage.setItem('locale', newLocale)
const locale = computed({
get: () => settingsStore.settings.locale,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
set: (value: string) => settingsStore.updateLocale(value)
})
const textSizeStore = useTextSizeStore()
const currentTextSize = textSizeStore.currentFontSize
const maxTextSize = textSizeStore.maxFontSize
const minTextSize = textSizeStore.minFontSize
</script>
<style scoped>
.fix-input-right :deep(.q-field__native) {
justify-content: end;
}
</style>
</style>

View File

@@ -1,82 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('subscribe__title')}}
</div>
</div>
</template>
<pn-scroll-list class="q-px-md">
<div id="subscribe-current-balance" class="flex w100 justify-between items-center no-wrap text-h6">
<span>
{{ $t('subscribe__current_balance') }}
</span>
<div class="flex items-center">
<q-icon name = "mdi-crown-circle-outline" color="orange" size="sm"/>
<div class="text-bold q-pa-xs ">
50
</div>
</div>
</div>
<div id="subscribe-need-tocken" :style = "{ borderLeft: 'solid 5px var(--q-info)' }" class="q-pl-sm">
<q-icon name = "mdi-crown-circle-outline" color="orange" size="xs"/>{{ $t('subscribe__token_formula') }}
<div class="text-caption">{{ $t('subscribe__token_formula_description') }}</div>
</div>
<div id="qty_chats" class="flex column q-pt-lg w100">
<div class="text-h6 flex items-center">
<span>{{ $t('account__chats') }}</span>
</div>
<div class="flex row justify-between">
<qty-chat-card
v-for = "chat in chats"
:key = chat.title
:qty = chat.qty
:bgColor = chat.color
:title = chat.title
/>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, inject, computed } from 'vue'
// import { useRouter } from 'vue-router'
// import { useAuthStore } from 'stores/auth'
// import type { WebApp } from '@twa-dev/types'
import qtyChatCard from 'components/admin/account-page/qtyChatCard.vue'
// import optionPayment from 'components/admin/account-page/optionPayment.vue'
// const router = useRouter()
// const authStore = useAuthStore()
// const tg = inject('tg') as WebApp
// const tgUser = tg.initDataUnsafe.user
const chats = ref([
{ title: 'account__chats_active', qty: 8, color: 'var(--q-primary)' },
{ title: 'account__chats_unbound', qty: 2, color: 'grey' },
{ title: 'account__chats_free', qty: 5, color: 'green' },
{ title: 'account__chats_total', qty: 15, color: 'var(--q-info)' },
])
/* const payment=ref([
{ id: 1, qty: 50, stars: 200, discount: 0 },
{ id: 2, qty: 120, stars: 400, discount: 20 },
{ id: 3, qty: 220, stars: 500, discount: 30 }
]) */
</script>
<style lang="scss">
</style>

236
src/pages/TaskAddPage.vue Normal file
View File

@@ -0,0 +1,236 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('task_add__title') }}
</div>
<q-btn
@click = "createTask()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<div class="flex column items-center q-ma-lg">
<div class="q-gutter-y-lg w100">
<q-input
v-model.trim="task.name"
dense
filled
autogrow
:label = "$t('task_add__name')"
class="bold-input w100"
/>
<q-input
v-model.trim="task.description"
dense
filled
autogrow
class = "w100"
:label = "$t('task_add__description')"
/>
<q-file
v-model="task.files"
:label="$t('task_add__attach_files')"
outlined
use-chips
multiple
dense
class="file-input-fix"
>
<template #append>
<q-icon name="attach_file"/>
</template>
</q-file>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__assigned_to')"
>
<template #prepend>
<q-icon name="mdi-account-arrow-left-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__watch')"
>
<template #prepend>
<q-icon name="mdi-account-eye-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
<q-input filled v-model="task.date" dense :label="$t('task_add__plan_date')">
<template #prepend>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="task.date" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
<template #append>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-time v-model="task.date" mask="YYYY-MM-DD HH:mm" format24h>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<div class="flex column">
<q-item-label header class="q-py-none q-my-none">
{{$t('task_add__priority')}}
</q-item-label>
<q-btn-toggle
v-model="task.priority"
no-caps
toggle-color="primary"
spread
unelevated
:options="priority"
class="q-mt-sm w100"
>
<template v-for="item in priority" :key="item.id" #[item.slot]>
<div class="row items-center no-wrap gap-xs text-weight-regular">
<span>
{{ $t(item.translationKey) }}
</span>
<pn-task-priority-icon :priority="item.value"/>
</div>
</template>
</q-btn-toggle>
</div>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('task_add__attached_chat')"
>
<template #prepend>
<q-icon name="mdi-chat-outline"/>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(task.company)).name }}
</template>
</q-select>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const task = ref({id: "p1", name: 'Кирюшкин Андрей', description: 'fdsfdsfsdfs', company: '', priority: 1, department: 'test', files: [], date: new Date(Date.now()).toLocaleString() })
const priority = [
{ id: 1, slot: 's1', label: '', translationKey: 'task_add__priority_normal', value: 0 },
{ id: 2, slot: 's2', label: '', translationKey: 'task_add__priority_important', value: 1 },
{ id: 3, slot: 's3', label: '', translationKey: 'task_add__priority_critical', value: 2 }
]
const companies = ref([
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
async function createTask () {
await router.push({ name: 'tasks' })
}
</script>
<style scoped>
.bold-input::v-deep .q-field__native {
font-weight: bold;
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('settings__title') }}
</div>
</div>
</template>
<pn-scroll-list>
<q-list separator>
<q-item>
<q-item-section avatar>
<q-avatar color="primary" rounded text-color="white" icon="mdi-translate" size="md" />
</q-item-section>
<q-item-section>
<span>{{ $t('settings__language') }}</span>
</q-item-section>
<q-item-section>
<q-select
class="fix-input-right text-body1"
v-model="locale"
:options="localeOptions"
dense
borderless
emit-value
map-options
hide-bottom-space
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-avatar color="primary" rounded text-color="white" icon="mdi-format-size" size="md" />
</q-item-section>
<q-item-section>
<span>{{ $t('settings__font_size') }}</span>
</q-item-section>
<q-item-section>
<div class="flex justify-end">
<q-btn
@click="textSizeStore.decreaseFontSize()"
color="negative" flat
icon="mdi-format-font-size-decrease"
class="q-pa-sm q-mx-xs"
:disable="currentTextSize <= minTextSize"
/>
<q-btn
@click="textSizeStore.increaseFontSize()"
color="positive" flat
icon="mdi-format-font-size-increase"
class="q-pa-sm q-mx-xs"
:disable="currentTextSize >= maxTextSize"
/>
</div>
</q-item-section>
</q-item>
</q-list>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { watch, ref } from 'vue'
import { useTextSizeStore } from 'src/stores/textSize'
const { locale } = useI18n()
const savedLocale = localStorage.getItem('locale') || 'en-US'
locale.value = savedLocale
const localeOptions = ref([
{ value: 'en-US', label: 'English' },
{ value: 'ru-RU', label: 'Русский' }
])
watch(locale, (newLocale) => {
localStorage.setItem('locale', newLocale)
})
const textSizeStore = useTextSizeStore()
const currentTextSize = textSizeStore.currentFontSize
const maxTextSize = textSizeStore.maxFontSize
const minTextSize = textSizeStore.minFontSize
</script>
<style scoped>
.fix-input-right :deep(.q-field__native) {
justify-content: end;
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('terms__title') }}
</div>
</div>
</template>
<pn-scroll-list>
{{ $t('under_construction') }}
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
</script>
<style>
</style>

168
src/pages/UserInfoPage.vue Normal file
View File

@@ -0,0 +1,168 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('user_card__title') }}
</div>
</div>
</template>
<pn-scroll-list>
<div
v-if="user"
class="flex column items-center q-pa-lg"
>
<q-avatar size="100px">
<q-img v-if="user.photo" :src="user.photo"/>
<pn-auto-avatar v-else :name="tname"/>
</q-avatar>
<div class="flex row items-start justify-center no-wrap q-pb-lg">
<div class="flex column justify-center">
<div class="text-bold q-pr-xs text-center" align="center" v-if="tname">{{ tname }}</div>
<div caption class="text-blue text-caption" align="center" v-if="user.username">@{{ user.username }}</div>
</div>
</div>
<div class="flex justify-around w100 q-pb-lg">
<q-btn
v-for="item in userActions"
:key="item.id"
@click="item.f"
round
:icon="item.icon"
color="primary"
/>
</div>
<div class="q-gutter-y-lg w100">
<q-field
v-for="(item, key) in displayUser"
:key
v-show="item"
:model-value="displayUser[key]"
dense
filled
class="w100"
:label = "$t('user_card__' + key)"
>
<template #control>
{{displayUser[key]}}
</template>
</q-field>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, inject } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUsersStore } from 'stores/users'
import type { User } from 'types/User'
import { parseIntString } from 'boot/helpers'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const router = useRouter()
const route = useRoute()
const usersStore = useUsersStore()
const user = ref<User>()
const userId = parseIntString(route.params.userId)
const tname = computed(() => {
return (!user.value)
? ''
: user.value.firstname
? user.value.lastname
? user.value.firstname + ' ' + user.value.lastname
: user.value.firstname
: user.value.lastname ?? ''
})
const userPosition = computed(() => {
return (!user.value)
? ''
: (user.value.company_id ? ' ' + user.value.company_id : '') +
(user.value.department ? ' ' + user.value.department : '') +
(user.value.role ? user.value.role : '')
})
const displayUser = computed(() => ({
name: user.value ? user.value.fullname : '',
phone: user.value ? user.value.phone : '',
email: user.value ? user.value.email : '',
position: userPosition.value
}))
const userActions = [
{ id: 0, icon: 'mdi-chat-outline', f: messageUser },
{ id: 1, icon: 'mdi-phone-outline', f: callUser },
{ id: 2, icon: 'mdi-share-variant-outline', f: shareUser }
]
onMounted(async () => {
if (userId && usersStore.userById(userId)) {
user.value = usersStore.userById(userId)
} else {
await abort()
}
})
async function abort () {
await router.replace({ name: 'files' })
}
function generateVCard () {
if (!user.value) return ''
const fields = [
'BEGIN:VCARD',
'VERSION:2.1',
'FN:'+ (user.value.fullname ?? ((user.value.firstname ?? '') + (user.value.lastname ?? ''))),
userPosition.value ? 'ORG:' + userPosition.value : '',
user.value.phone ? 'TEL;WORK;VOICE:' + user.value.phone : '',
user.value.email ? 'EMAIL:' + user.value.email : '',
'END:VCARD'
]
return fields
.filter(el => el !== '')
.join('\n')
}
function messageUser () {
const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? user.value.telegram_id))
if (tg?.platform !== 'unknown') {
tg?.openLink(telegramUrl, { try_instant_view: true })
} else {
window.open(telegramUrl, '_blank')
}
}
function callUser () {
const telegramUrl = 'tg://call?peer_id=' + (user.value ? user.value.telegram_id : '')
if (tg?.platform !== 'unknown') {
tg?.openLink(telegramUrl, { try_instant_view: true })
} else {
window.open(telegramUrl, '_blank')
}
}
function shareUser () {
// не работает, отправляет текст
const tgShareUrl = 'https://t.me/share/url?url= &text=' + encodeURIComponent(generateVCard())
tg.openTelegramLink(tgShareUrl)
}
</script>
<style>
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-ma-md justify-between">
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('chats__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</div>
</template>
<q-list separator>
<q-item
v-for="item in displayChats"
:key="item.id"
clickable
v-ripple
@click="goChat(item.invite_link)"
>
<q-item-section avatar>
<q-avatar rounded>
<q-img v-if="item.logo" :src="item.logo"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">
{{ item.name }}
</q-item-label>
<q-item-label caption lines="2">
{{ item.description }}
</q-item-label>
</q-item-section>
<q-item-section side top>
<div class="flex items-end column">
<span class="text-caption flex items-center">
<q-icon name="mdi-account-outline" color="grey" />
<span>{{ item.user_count}}</span>
</span>
</div>
</q-item-section>
</q-item>
</q-list>
</pn-scroll-list>
</div>
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue'
import { useChatsStore } from 'stores/chats'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const search = ref('')
const chatsStore = useChatsStore()
const chats = computed(() => chatsStore.chats)
const displayChats = computed(() => {
if (!search.value || !(search.value && search.value.trim())) return chats.value
const searchValue = search.value.trim().toLowerCase()
const arrOut = chats.value
.filter(el =>
(el.name && el.name.toLowerCase().includes(searchValue)) ||
(el.description && el.description.toLowerCase().includes(searchValue))
)
return arrOut
})
function goChat (invite: string) {
tg.openTelegramLink(invite)
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,504 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
<q-btn
icon="mdi-calendar-month-outline"
flat dense round
class="q-mr-sm"
size="lg"
:color="showCalendar ? 'primary' : 'grey'"
@click="showCalendar = !showCalendar"
>
<div>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -16px; margin-left: -12px"
:style="{ opacity: datesRange ? 0.8 : 0 }"
/>
</div>
</q-btn>
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('files__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
<q-btn
@click="showFiltersDialog = true"
icon="mdi-filter-outline"
dense round flat
size="lg"
:color="showFiltersDialog ? 'primary' : 'grey'"
class="q-mr-xs"
>
<div>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -16px; margin-left: -12px"
:style="{ opacity: !checkFiltersSelect ? 0.8 : 0 }"
/>
</div>
</q-btn>
</div>
<q-slide-transition>
<div v-show="showCalendar">
<q-date
class="w100 fix-calendar q-mb-sm q-mt-xs"
first-day-of-week="1"
v-model="datesRange"
range
flat
:events="filesDates"
event-color="brand"
today-btn
minimal
dense
/>
</div>
</q-slide-transition>
</template>
<q-list separator>
<q-item
v-for="item in displayFiles"
:key="item.id"
:clickable="false"
>
<q-item-section avatar class="items-center" >
<q-avatar rounded>
<q-icon
:name="fileIcon(item.filename).icon"
:style="{color: fileIcon(item.filename).color}"
size="md"
/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">
{{ parseFileName(item.filename).name }}
</q-item-label>
<q-item-label caption lines="1">
<div class="flex row no-wrap items-center w100">
<div class="second-line-item flex no-wrap items-center q-mr-sm">
<q-icon
:name="item.parent_type === 0
? 'mdi-message-outline'
: item.parent_type === 1
? 'mdi-clipboard-outline'
: 'mdi-calendar-month-outline'
"
class="q-mr-none"
/>
<span class="ellipsis">{{ fileFrom(item.chat_id) }}</span>
</div>
<div class="second-line-item flex no-wrap items-center">
<q-icon name="mdi-account-outline" class="q-mr-none"/>
<span class="ellipsis">{{ fileBy(item.published_by) }}</span>
</div>
</div>
</q-item-label>
<q-item-label caption lines="1">
<div class = "flex justify-between items-center">
<span>{{ fileSize(item.size) }}</span>
<span>{{ fileDate(item.published) }}</span>
</div>
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</pn-scroll-list>
<q-dialog
v-model="showFiltersDialog"
maximized
transition-show="slide-up"
transition-hide="slide-down"
>
<q-card
class="q-mt-xl top-rounded-card flex column fix-card-width no-scroll no-wrap"
style="border-top-left-radius: var(--top-raduis) !important; border-top-right-radius: var(--top-raduis) !important;">
<q-card-section>
<div class="flex items-center no-wrap justify-between w100">
<div class="text-h6">{{ t('files__filters') }}</div>
<div>
<q-btn
v-if="!checkFiltersSelect"
@click="resetFilters"
flat
no-caps
dense
color="grey-6"
class="q-mr-lg"
>
{{ t('files_filters_reset')}}
</q-btn>
<q-btn
:icon="checkFiltersSelect ? 'mdi-close' :'mdi-check'"
@click="showFiltersDialog=false"
flat round
/>
</div>
</div>
</q-card-section>
<div class="col-grow q-px-none q-ma-none">
<q-resize-observer @resize="onResize" />
<q-card-section
class="q-pt-none q-pb-md q-px-md q-ma-none scroll"
:style="{ height: 'calc(' + dialogSectionHeight + 'px' +' - 32px)' }"
>
<div class="q-pl-sm text-bold">{{ t('files__filters_extension') }}</div>
<div class="flex row">
<div
v-for="(item,idx) in fileExtExample"
:key="idx"
>
<q-icon
:name="fileIcon(item).icon"
:style="{ color: fileIcon(item).color }"
@click = "updateFileExtFilters(fileIcon(item).type)"
:class="fileExtFilters.includes(fileIcon(item).type) ? 'active' : 'inactive'"
size="md"
class="q-pa-sm"
/>
</div>
</div>
<div class="q-pl-sm q-mt-md text-bold">{{ t('files__filters_source') }}</div>
<div class="flex column">
<div
v-for="(item,idx) in fileSourceOptions"
:key="idx"
>
<q-checkbox
v-model="fileSourceFilters"
:val="item.value"
>
{{ t(item.label) }}
</q-checkbox>
</div>
</div>
<div class="q-pl-sm q-mt-md text-bold">{{ t('files__filters_by') }}</div>
<q-select
filled
v-model="fileByFilters"
:options="fileByOptions"
multiple
use-chips
dense
class="w100"
/>
<div class="q-pl-sm q-mt-md text-bold">{{ t('files__filters_size') }}</div>
<div class="flex column">
<div
v-for="(item,idx) in fileSizeOptions"
:key="idx"
>
<q-checkbox
v-model="fileSizeFilters"
:val="item.value"
:label = "t(item.label)"
/>
</div>
</div>
</q-card-section>
</div>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFilesStore } from 'stores/files'
import { useI18n } from 'vue-i18n'
import { date } from 'quasar'
import type { File } from 'types/File'
const { t }= useI18n()
const search = ref('')
const filesStore = useFilesStore()
const files = computed(() => filesStore.files)
const showCalendar = ref<boolean>(false)
const showFiltersDialog = ref(false)
const datesRange = ref<null | { from: string, to: string }>(null)
const displayFiles = computed(() => {
return files.value
.filter(searchFiles)
.filter(fileExt)
.filter(fileSource)
.filter(fileSize)
.filter(checkDateInterval)
function searchFiles (el: File) {
if (!search.value || !(search.value && search.value.trim())) return true
const searchValue = search.value.trim().toLowerCase()
return el.filename.toLowerCase().includes(searchValue)
}
function checkDateInterval (el: File) {
if (!datesRange.value) return true
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
return (from < el.published) && ( to >= el.published)
}
function fileExt (el: File) {
if (fileExtFilters.value.length === 0) return true
const type = fileIcon(el.filename).type
return fileExtFilters.value.includes(type)
}
function fileSource (el: File) {
if (fileSourceFilters.value.length === 0) return true
return fileSourceFilters.value.includes(el.parent_type)
}
function fileSize (el: File) {
if (fileSizeFilters.value.length === 0) return true
const fileSize = el.size
const sortedFilters = [...fileSizeFilters.value].sort((a, b) => a - b)
const ranges = fileSizeOptions.map((option, index) => ({
min: index === 0 ? 0 : fileSizeOptions[index - 1]!.value,
max: option.value,
value: option.value
}))
return ranges.some(range =>
sortedFilters.includes(range.value) &&
(fileSize > range.min &&
(range.max === Infinity ? true : fileSize <= range.max))
)
}
})
const fileExtFilters = ref<string[]>([])
const fileExtExample = [
'1.common', '2.doc', '3.xls', '4.vsd', '5.ppt', '6.pdf', '7.png', '8.mp3', '9.mp4', '10.js', '11.txt', '12.zip', '13.skp', '14.dwg', '15.ttf'
]
function updateFileExtFilters (select: string) {
if (fileExtFilters.value.includes(select)) {
const idx = fileExtFilters.value.indexOf(select)
fileExtFilters.value.splice(idx, 1)
} else fileExtFilters.value.push(select)
}
const fileSourceFilters = ref<number[]>([])
const fileSourceOptions = [
{ id: 1, icon: 'mdi-message-outline', value: 0, label: 'files__filters_source_chats' },
{ id: 2, icon: 'mdi-clipboard-outline', value: 1, label: 'files__filters_source_tasks' },
{ id: 3, icon: 'mdi-calendar-month-outline', value: 2, label: 'files__filters_source_meetings' }
]
const fileSizeFilters = ref<number[]>([])
const fileSizeOptions = [
{ id: 1, value: 5242880, label: 'files__filters_size_small' }, // 5MB
{ id: 2, value: 26214400, label: 'files__filters_size_middle' }, // 25MB
{ id: 3, value: 104857600, label: 'files__filters_size_big' }, // 100MB
{ id: 4, value: Infinity, label: 'files__filters_size_very_big' } // more 100 MB
]
const fileByFilters = ref<number[]>([]) // user ids
const fileByOptions = <object>[] // temp obj, need users store!!!
const checkFiltersSelect = computed(() => (
(fileExtFilters.value.length === 0) &&
(fileByFilters.value.length === 0) &&
(fileSourceFilters.value.length === 0) &&
(fileSizeFilters.value.length === 0)
))
const resetFilters = () => {
fileExtFilters.value = []
fileByFilters.value= []
fileSourceFilters.value = []
fileSizeFilters.value = []
}
const filesDates = computed(() => displayFiles.value.map(el => date.formatDate(el.published, 'YYYY/MM/DD')))
interface ParsedFile {
name: string
ext: string
}
function parseFileName(filename: string): ParsedFile {
const lastDotIndex = filename.lastIndexOf('.')
if (lastDotIndex === -1) {
return { name: filename, ext: '' }
}
return {
name: filename.slice(0, lastDotIndex),
ext: filename.slice(lastDotIndex + 1).toLowerCase()
}
}
interface FileIcon {
type: string
icon: string
color: string
}
function fileIcon(filename: string): FileIcon {
const ext = parseFileName(filename).ext;
switch (ext) {
case 'doc':
case 'docx':
return { type: 'doc', icon: 'pn-icon-file-doc', color: '#2B579A' }
case 'xls':
case 'xlsx':
case 'csv':
return { type: 'xls', icon: 'pn-icon-file-xls', color: '#217346' }
case 'vsd':
case 'vsdx':
return { type: 'vsd', icon: 'pn-icon-file-vsd', color: '#3955A3' }
case 'ppt':
case 'pptx':
return { type: 'ppt', icon: 'pn-icon-file-ppt', color: '#D24726' }
case 'pdf':
return { type: 'pdf', icon: 'pn-icon-file-pdf', color: '#D0021B' }
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'bmp':
case 'svg':
return { type: 'img', icon: 'pn-icon-file-img', color: '#4CAF50' }
case 'mp3':
case 'wav':
case 'ogg':
return { type: 'music', icon: 'pn-icon-file-audio', color: '#FF9800' }
case 'mp4':
case 'avi':
case 'mov':
case 'mkv':
return { type: 'video', icon: 'pn-icon-file-video', color: '#9C27B0' }
case 'js':
case 'ts':
case 'html':
case 'css':
case 'json':
case 'xml':
return { type: 'code', icon: 'pn-icon-file-code', color: '#999' }
case 'txt':
return { type: 'txt', icon: 'pn-icon-file-txt', color: '#757575' }
case 'zip':
case 'rar':
case '7z':
case 'tar':
case 'gz':
return { type: 'archive', icon: 'pn-icon-file-archive', color: '#F78E1E' }
case 'skp':
return { type: 'skp', icon: 'pn-icon-file-skp', color: '#CC0000' }
case 'dwg':
case 'dxf':
return { type: 'cad', icon: 'pn-icon-file-dwg', color: '#00579D' }
case 'ttf':
return { type: 'font', icon: 'pn-icon-file-ttf', color: '#607D8B' }
default:
return { type: 'common', icon: 'pn-icon-file-default', color: '#9E9E9E' }
}
}
function fileFrom (value: number): string {
return String(value)
}
function fileBy (value: number): string {
return String(value)
}
function fileSize (value: number): string {
if (value === 0) return '-'
const units = ['B', 'kB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(value) / Math.log(1024))
const index = Math.min(i, units.length - 1)
const result = (value / Math.pow(1024, index)).toFixed(2)
return `${result} ${t(units[index]??'')}`
}
function fileDate (value: number): string {
return date.formatDate(value, 'DD-MM-YY')
}
const dialogSectionHeight = ref<number>(0)
interface sizeParams {
height: number,
width: number
}
function onResize (size: sizeParams) {
dialogSectionHeight.value = size.height
}
</script>
<style scoped>
.second-line-item {
display: flex;
align-items: center;
flex: 0 1 auto;
min-width: 0;
max-width: 60%;
}
.fix-card-width {
width: var(--body-width) !important;
}
.active {
opacity: 1
}
.inactive {
opacity: 0.4
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<div
id="project-info"
:style="{ height: headerHeight + 'px', minHeight: '48px' }"
class="flex row items-center justify-between no-wrap w100 q-gutter-x-sm"
style="overflow: hidden; transition: height 0.3s ease-in-out; margin-left: 0"
>
<div class="ellipsis overflow-hidden q-pa-none q-ma-none">
<q-resize-observer @resize="onResize" />
<transition
enter-active-class="animated slideInUp"
leave-active-class="animated slideOutUp"
mode="out-in"
>
<div
v-if="!expandProjectInfo"
@click="toggleExpand"
class="text-h6 ellipsis no-wrap w100 q-pa-none q-ma-none"
key="compact"
>
{{ project.name }}
</div>
<div
v-else
class="flex items-center no-wrap q-hoverable q-animate--slideUp q-py-sm"
@click="toggleExpand"
key="expanded"
>
<q-avatar rounded>
<img v-if="project.logo" :src="project.logo" style="object-fit: cover;"/>
<pn-auto-avatar v-else :name="project.name"/>
</q-avatar>
<div class="flex column text-white fit">
<div
class="text-h6 q-pl-sm"
:style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }"
>
{{ project.name }}
</div>
<div class="text-caption q-pl-sm" :style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }">
{{ project.description }}
</div>
</div>
</div>
</transition>
</div>
<q-btn
@click="drawer=!drawer"
:class="drawer ? 'text-grey' : 'text-white'"
flat
round
icon="mdi-briefcase-outline"
size="md"
class="q-ml-xl"
/>
</div>
<q-drawer
v-model="drawer"
side="right"
overlay
bordered
class="bg-grey-3 text-black text-body1 no-scroll"
>
<div
class="flex items-center justify-between q-my-sm q-mx-md text-h6"
>
{{ $t('header__my_projects') }}
<q-btn
icon="mdi-close"
flat round
@click="drawer = false"
:ripple="false"
/>
</div>
<q-scroll-area
style="height: 100vh"
class="fix-width-scroll"
>
<q-list separator>
<q-item
v-for = "item in projects"
:key="item.id"
clickable
v-ripple
@click="changeProject(item.id)"
>
<q-item-section avatar>
<q-avatar rounded >
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="height: 40px"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
</q-item-section>
<q-item-section :class="item.id === currentProjectId ? 'text-primary !important' : ''">
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label class="text-caption" lines="2">{{item.description}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
</q-drawer>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useProjectsStore } from 'stores/projects'
import { useRouter, useRoute } from 'vue-router'
const drawer=ref<boolean>(false)
const projectsStore = useProjectsStore()
const expandProjectInfo = ref<boolean>(false)
const headerHeight = ref<number>(0)
const currentProjectId = computed(() => projectsStore.currentProjectId)
const projects = computed(() => projectsStore.projects)
const project = computed(() => {
const currentProject = currentProjectId.value && projectsStore.projectById(currentProjectId.value)
return currentProject
? {
name: currentProject.name,
description: currentProject.description ?? '',
logo: currentProject.logo ?? ''
}
: {
name: '',
description: '',
logo: ''
}
})
function toggleExpand () {
expandProjectInfo.value = !expandProjectInfo.value
}
const router = useRouter()
const route = useRoute()
function changeProject(id: number) {
drawer.value = false
router.push({ name: route.name, params: { id }})
}
interface sizeParams {
height: number,
width: number
}
function onResize (size :sizeParams) {
headerHeight.value = size.height
}
</script>
<style scoped>
.fix-width-scroll :deep(.q-scrollarea__content){
width: 100%
}
</style>

View File

@@ -0,0 +1,545 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
<q-btn
icon="mdi-calendar-month-outline"
flat dense round
class="q-mr-sm"
size="lg"
:color="showCalendar ? 'primary' : 'grey'"
@click="showCalendar = !showCalendar"
>
<div>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -16px; margin-left: -12px"
:style="{ opacity: datesRange ? 0.8 : 0 }"
/>
</div>
</q-btn>
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('meetings__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</div>
<q-slide-transition>
<div v-show="showCalendar">
<q-date
class="w100 fix-calendar q-mb-sm q-mt-xs"
first-day-of-week="1"
v-model="datesRange"
range
flat
:events="meetingsDates"
event-color="brand"
today-btn
minimal
dense
/>
</div>
</q-slide-transition>
</template>
<div class="w100 flex justify-between q-px-md">
<div>
<q-btn flat dense no-caps @click="showPreviousMeetings" v-if="countPreviousMeetings !==0">
<span class="text-caption text-grey">{{$t('meetings__previous')}}
({{ countPreviousMeetings > 5 ? '5+' : countPreviousMeetings }})
</span>
</q-btn>
</div>
<q-btn flat dense no-caps v-if ="showReset" @click="resetDisplayPreviousMeetings">
<div class="flex items-center text-caption text-grey">
{{$t('meetings__previous_hide')}}
<q-icon name="close" size="xs"/>
</div>
</q-btn>
</div>
<q-list separator>
<template v-for="(el, idx) in displayMeetingsWithHeader" :key="el.month">
<div
class="q-mx-md text-caption orline"
:class="idx === 0 ? 'q-pt-none' : 'q-pt-md'"
>
<span class="q-mx-md">{{ el.month }}</span>
</div>
<template v-for="item in el.meetings" :key="item.id">
<q-slide-item
@right="handleSlideRight($event, item.id)"
@left="handleSlideLeft($event, item.id)"
clickable
v-ripple
@click="goMeeting(item.id)"
right-color="red"
left-color="green"
>
<template #right v-if="!item.is_cancel">
<q-icon size="lg" name="mdi-calendar-remove-outline"/>
</template>
<template #left v-if="!item.is_cancel">
<q-icon size="lg" name="mdi-calendar-refresh-outline"/>
</template>
<q-item
:key="item.id"
:style = "{
backgroundColor:
item.is_cancel
? '#999'
: item.meet_date < Date.now()
? '#eee'
: 'inherit',
border: item.is_cancel ? 'solid 1px #999' : 'inherit'
}"
>
<q-item-section avatar>
<div class="flex column items-end">
<div
class="text-caption"
:class="item.isWeekend ? 'text-red' : 'text-grey'"
>
{{ item.dayOfWeek }}
</div>
<div class="flex items-start justify-start">
<div
class="text-bold text-h5 flex items-center"
style="line-height: 1rem;"
:style="{ color: getDayColorStatus(item.meet_date)}"
>
{{ item.day }}
</div>
</div>
<div class="text-caption">
{{ item.time }}
</div>
</div>
</q-item-section>
<q-item-section>
<q-item-label v-if="item.is_cancel" lines="1" class="text-negative flex items-center text-caption">
<q-icon name="mdi-calendar-remove-outline" class="q-mr-none"/>
{{ $t('meeting_info__canceled') }}
</q-item-label>
<q-item-label lines="1" class="text-bold">
{{ item.name }}
</q-item-label>
<q-item-label lines="1" class="text-caption" v-if="item.place">
<q-icon name="mdi-map-marker-outline" class="q-mr-none"/>
<span class="ellipsis">{{ item.place }}</span>
</q-item-label>
<q-item-label caption lines="1">
<div class="flex row no-wrap items-center w100">
<div class="second-line-item flex no-wrap items-center q-mr-sm">
<q-icon name="mdi-account-tie-outline" class="q-mr-none"/>
<span class="ellipsis">{{ usersStore.userNameById(item.created_by) }}</span>
</div>
<div class="second-line-item flex no-wrap items-center" v-if="item.chat_attach">
<q-icon name="mdi-message-outline" class="q-mr-none"/>
<span class="ellipsis">{{ chatsStore.chatById(item.chat_attach)?.name }}</span>
</div>
</div>
</q-item-label>
</q-item-section>
<q-item-section side top>
<div class="flex items-end column">
<span class="text-caption flex items-center">
<q-icon name="mdi-account-outline" color="grey" />
<span>{{ item.participants?.length }}</span>
</span>
<span class="text-caption flex items-center" v-if="item.files && item.length !== 0">
<q-icon name="mdi-paperclip" color="grey" />
<span>{{ item.files.length }}</span>
</span>
</div>
</q-item-section>
</q-item>
</q-slide-item>
</template>
</template>
</q-list>
</pn-scroll-list>
<q-page-sticky
position="bottom-right"
:offset="[0, 18]"
class="fix-fab-offset"
>
<transition
appear
enter-active-class="animated zoomIn"
>
<q-btn
v-if="showFab"
fab
icon="add"
color="brand"
@click="createMeeting()"
/>
</transition>
</q-page-sticky>
</div>
<q-dialog
v-model="showDialogDeleteMeeting"
@before-hide="onDialogBeforeHide()"
>
<pn-dialog-body
icon="mdi-calendar-remove-outline"
color="negative"
title="meeting_info__dialog_cancel_title"
>
<template #title>
</template>
<template #actions>
<div class="flex no-wrap w100 justify-center q-gutter-x-md">
<q-btn
:label="$t('meeting_info__dialog_cancel_delete')"
outline
color="grey"
v-close-popup
rounded
class="w50"
@click="onConfirmDelete()"
/>
<q-btn
:label="$t('meeting_info__dialog_cancel_ok')"
color="negative"
v-close-popup
rounded
class="w50"
@click="onConfirmCancel()"
/>
</div>
<q-btn
class="w80 q-mt-md q-mb-sm" flat
v-close-popup rounded
no-caps
@click="onCancel()"
>
<div class="flex items-center">
{{$t('close')}}
<q-icon name="close"/>
</div>
</q-btn>
</template>
</pn-dialog-body>
</q-dialog>
<q-dialog
v-model="showDialogRestoreMeeting"
@before-hide="onDialogBeforeHide()"
>
<pn-dialog-body
icon="mdi-calendar-refresh-outline"
color="green"
title="meeting_info__dialog_restore_title"
>
<template #title>
</template>
<template #actions>
<q-btn
:label="$t('meeting_info__dialog_restore_ok')"
color="green"
v-close-popup
rounded
class="w80 q-mb-md"
@click="onConfirmRestore()"
/>
<q-btn
class="w80 q-mb-sm" flat
v-close-popup rounded
no-caps
@click="onCancel()"
>
<div class="flex items-center">
{{$t('close')}}
<q-icon name="close"/>
</div>
</q-btn>
</template>
</pn-dialog-body>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, watch } from 'vue'
import { useMeetingsStore } from 'stores/meetings'
import { useUsersStore } from 'stores/users'
import { useChatsStore } from 'stores/chats'
import { useRouter } from 'vue-router'
import type { Meeting } from 'types/Meeting'
import { date } from 'quasar'
const search = ref('')
const showCalendar = ref<boolean>(false)
const datesRange = ref<null | { from: string, to: string }>(null)
const deleteMeetingId = ref<number | undefined>(undefined)
const restoreMeetingId = ref<number | undefined>(undefined)
const showDialogDeleteMeeting = ref<boolean>(false)
const showDialogRestoreMeeting = ref<boolean>(false)
const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false)
const meetingsStore = useMeetingsStore()
const usersStore = useUsersStore()
const chatsStore = useChatsStore()
const router = useRouter()
interface SlideEvent {
reset: () => void
}
interface enrichMeeting extends Meeting {
monthYear: string
day: string
isWeekend: boolean
dayOfWeek: string
time: string
}
const meetings = computed(() => meetingsStore.meetings)
const meetingsDates = computed(() => displayMeetingsAll.value.map(el => date.formatDate(el.meet_date, 'YYYY/MM/DD')))
const displayMeetingsAll = computed(() => {
return meetings.value
.filter(searchMeetings)
.filter(checkDateInterval)
.sort(sortByTime)
.map(enrich)
function searchMeetings (el: Meeting) {
if (!search.value || !(search.value && search.value.trim())) return true
const searchValue = search.value.trim().toLowerCase()
return (
el.name.toLowerCase().includes(searchValue) ||
el.description && el.description.toLowerCase().includes(searchValue)
)
}
function checkDateInterval (el: Meeting) {
if (!datesRange.value) return true
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
return (from < el.meet_date) && ( to >= el.meet_date)
}
function sortByTime (a: Meeting, b: Meeting) {
return a.meet_date - b.meet_date
}
function enrich (el: Meeting) {
return {
...el,
monthYear: date.formatDate(el.meet_date, 'MMMM YYYY'),
day: date.formatDate(el.meet_date, 'D'),
isWeekend: (date.formatDate(el.meet_date, 'E') === '6'|| date.formatDate(el.meet_date, 'E') === '7'),
dayOfWeek: date.formatDate(el.meet_date, 'ddd'),
time: date.formatDate(el.meet_date, 'HH:mm')
}
}
})
const displayTime = ref(Date.now())
const currentMeetings = computed(() =>
displayMeetingsAll.value.filter(el => el.meet_date >= displayTime.value)
)
const countPreviousMeetings = computed(() =>
displayMeetingsAll.value.length - currentMeetings.value.length
)
const showReset = computed(() =>
currentMeetings.value.length !== displayMeetingsAll.value.filter(el => el.meet_date >= Date.now()).length
)
function showPreviousMeetings () {
if (countPreviousMeetings.value === 0) return
const newIndex = Math.max(0, countPreviousMeetings.value - 5)
displayTime.value = displayMeetingsAll.value[newIndex]?.meet_date || Date.now()
}
function resetDisplayPreviousMeetings () {
displayTime.value = Date.now()
}
watch([search, datesRange], () => {
resetDisplayPreviousMeetings()
})
const displayMeetingsWithHeader = computed(() => {
type ResultItem = { month: string, meetings: enrichMeeting[] }
if (!currentMeetings.value.length) return []
const result: ResultItem[] = []
let currentMonth: string | null = null
let currentGroup: ResultItem
currentMeetings.value.forEach((el: enrichMeeting) => {
const month = date.formatDate(el.meet_date, 'MMMM YYYY')
if (month !== currentMonth) {
currentMonth = month
currentGroup = {
month: month,
meetings: []
}
result.push(currentGroup)
}
currentGroup.meetings.push(el)
})
return result
})
function getDayColorStatus (time: number) {
const now = new Date()
const timeDate = new Date(time)
const diffMinutes = Math.abs(date.getDateDiff(now, timeDate, 'minutes'))
if (diffMinutes <= 90) return 'var(--q-negative)'
const startOfToday = new Date()
const utcDay = now.getUTCDay()
const daysUntilSunday = utcDay === 0 ? 0 : 7 - utcDay
const endOfWeek = new Date(startOfToday)
endOfWeek.setUTCDate(endOfWeek.getUTCDate() + daysUntilSunday)
endOfWeek.setUTCHours(23, 59, 59, 999)
if (time >= startOfToday.getTime() && time <= endOfWeek.getTime()) {
return 'var(--q-primary)'
}
return ''
}
async function goMeeting (meetingId: number) {
await new Promise(resolve => setTimeout(resolve, 300))
await router.push({ name: 'meeting_info', params: { meetingId }})
}
async function createMeeting () {
await router.push({ name: 'meeting_add'})
}
function handleSlideRight (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogDeleteMeeting.value = true
deleteMeetingId.value = id
}
function handleSlideLeft (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogRestoreMeeting.value = true
restoreMeetingId.value = id
}
function onDialogBeforeHide () {
if (!closedByUserAction.value) {
onCancel()
}
closedByUserAction.value = false
}
function onCancel() {
closedByUserAction.value = true
if (currentSlideEvent.value) {
currentSlideEvent.value.reset()
currentSlideEvent.value = null
}
}
async function onConfirmDelete() {
closedByUserAction.value = true
if (deleteMeetingId.value) {
await meetingsStore.remove(deleteMeetingId.value)
}
currentSlideEvent.value = null
}
async function onConfirmCancel() {
closedByUserAction.value = true
if (deleteMeetingId.value) {
await meetingsStore.setCancelStatus(deleteMeetingId.value, true)
}
currentSlideEvent.value = null
}
async function onConfirmRestore() {
closedByUserAction.value = true
if (restoreMeetingId.value) {
await meetingsStore.setCancelStatus(restoreMeetingId.value, false)
}
currentSlideEvent.value = null
}
// fix fab jumping
const showFab = ref(false)
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
onActivated(() => {
timerId.value = setTimeout(() => {
showFab.value = true
}, 300)
})
onDeactivated(() => {
showFab.value = false
if (timerId.value) {
clearTimeout(timerId.value)
timerId.value = null
}
})
onBeforeUnmount(() => {
if (timerId.value) clearTimeout(timerId.value)
})
</script>
<style scoped lang="scss">
/* fix mini border after slide */
:deep(.q-slide-item__right) {
align-self: center;
height: 98%;
}
:deep(.q-slide-item__left) {
align-self: center;
height: 98%;
}
.fix-calendar :deep(.q-date__view) {
padding-top: 0;
padding-bottom: 0;
min-height: auto;
}
.fix-calendar :deep(.q-date__calendar-days-container) {
min-height: auto;
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
<q-btn
icon="mdi-calendar-month-outline"
flat dense round
class="q-mr-sm"
size="lg"
:color="showCalendar ? 'primary' : 'grey'"
@click="showCalendar = !showCalendar"
/>
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('tasks__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
<q-btn
@click="showFilters = true"
icon="mdi-filter-outline"
dense round flat
size="lg"
:color="showFilters ? 'primary' : 'grey'"
class="q-mr-xs"
>
<div>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -16px; margin-left: -12px"
:style="{ opacity: selectedFilters.length !== 0 ? 0.8 : 0 }"
/>
</div>
</q-btn>
</div>
<q-slide-transition>
<div v-show="showCalendar">
<q-date
class="w100 fix-calendar q-mb-sm q-mt-xs"
first-day-of-week="1"
v-model="date"
minimal
dense
/>
</div>
</q-slide-transition>
</template>
<q-list bordered separator>
<q-item
v-for="item in displayTasks"
:key="item.id"
clickable v-ripple
@click="goTask(item.id)"
>
<task-item :item/>
</q-item>
</q-list>
<div class="q-py-lg"/> <!-- fix hide scroll area by tabs -->
</pn-scroll-list>
<q-page-sticky
position="bottom-right"
:offset="[0, 18]"
class="fix-fab-offset"
>
<transition
appear
enter-active-class="animated zoomIn"
>
<q-btn
v-if="showFab"
fab
icon="add"
color="brand"
@click="createTask()"
/>
</transition>
</q-page-sticky>
</div>
<q-dialog
v-model="showFilters"
position="bottom"
>
<div class="w100 filter-panel top-rounded-card bg-white q-pa-md">
<div class="flex justify-between items-center">
<span class="text-h6">{{ $t('tasks__filters') }}</span>
<q-btn
@click="showFilters = false"
flat dense round
size="md"
icon="close"
/>
</div>
<q-list class="w100">
<template v-for="filter in filters" :key="filter.id">
<q-item-label header v-if="filter.header">{{$t(filter.label)}}</q-item-label>
<q-item v-else class="q-px-none">
<q-item-section side>
<q-checkbox
v-model="selectedFilters"
:val="filter.id"
dense
class="q-px-sm"
/>
</q-item-section>
<q-item-section>
<q-item-label class="flex items-center">
<span>
{{ $t(filter.label) }}
</span>
<pn-task-priority-icon :priority="filter.priority"/>
</q-item-label>
</q-item-section>
</q-item>
</template>
</q-list>
</div>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
import { useTasksStore } from 'stores/tasks'
import { useRouter } from 'vue-router'
import taskItem from 'components/taskItem.vue'
import type { Task } from 'types/Task'
const search = ref('')
const showCalendar = ref<boolean>(false)
const date = ref(Date.now())
const tasksStore = useTasksStore()
const router = useRouter()
const tasks = computed(() => tasksStore.tasks)
// filter
const showFilters = ref(false)
const selectedFilters = ref([])
const filters = [
{ id: 'taskType', label: 'tasks__filters_types', header: true },
{ id: 'taskIn', label: 'tasks__filters_in' },
{ id: 'taskOut', label: 'tasks__filters_out' },
{ id: 'taskWatch', label: 'tasks__filters_watch' },
{ id: 'taskPriority', label: 'tasks__filters_priority', header: true },
{ id: 'taskPriorityNormal', label: 'tasks__filters_priority_normal' },
{ id: 'taskPriorityImportant', label: 'tasks__filters_priority_important', priority: 1 },
{ id: 'taskPriorityCritical', label: 'tasks__filters_priority_critical', priority: 2 }
]
const displayTasks = computed(() => {
let filteredTasks = [...tasks.value]
if (selectedFilters.value.length > 0) {
const filterFunctions = selectedFilters.value.map((filterId: string) => {
switch(filterId) {
case 'taskPriorityNormal':
return (task: Task) => task.priority === 0 || !task.priority
case 'taskPriorityImportant':
return (task: Task) => task.priority === 1
case 'taskPriorityCritical':
return (task: Task) => task.priority === 2
/* case 'taskIn':
return (task: Task) => task.type === 'in'
case 'taskOut':
return (task: Task) => task.type === 'out'
case 'taskWatch':
return (task: Task) => task.type === 'watch' */
default:
return () => true
}
})
filteredTasks = filteredTasks.filter(task =>
filterFunctions.every(filterFn => filterFn(task))
)
}
if (search.value?.trim()) {
const searchValue = search.value.trim().toLowerCase()
filteredTasks = filteredTasks.filter(task =>
task.name.toLowerCase().includes(searchValue) ||
(task.description && task.description.toLowerCase().includes(searchValue))
)
}
return filteredTasks
})
async function goTask (taskId: number) {
await router.push({ name: 'task_info', params: { taskId }})
}
async function createTask () {
await router.push({ name: 'task_add'})
}
// fix fab jumping
const showFab = ref(false)
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
onActivated(() => {
timerId.value = setTimeout(() => {
showFab.value = true
}, 300)
})
onDeactivated(() => {
showFab.value = false
if (timerId.value) {
clearTimeout(timerId.value)
timerId.value = null
}
})
onBeforeUnmount(() => {
if (timerId.value) clearTimeout(timerId.value)
})
</script>
<style scoped lang="scss">
/* fix mini border after slide */
:deep(.q-slide-item__right)
{
align-self: center;
height: 98%;
}
.fix-calendar :deep(.q-date__view) {
padding-top: 0;
padding-bottom: 0;
min-height: auto;
}
.fix-calendar :deep(.q-date__calendar-days-container) {
min-height: auto;
}
.filter-panel {
max-height: 70vh;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-ma-md justify-between">
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('users__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</div>
</template>
<q-list separator>
<q-item
v-for="item in displayUsers"
:key="item.id"
v-ripple
clickable
@click="goUserInfo(item.id)"
>
<q-item-section avatar>
<q-avatar>
<q-img v-if="item.photo" :src="item.photo" fit="cover"/>
<pn-auto-avatar v-else :name="item.section1"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold" v-if="item.section1">
{{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>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUsersStore } from 'stores/users'
import type { User } from 'types/User'
const router = useRouter()
const search = ref('')
const usersStore = useUsersStore()
const users = computed(() => usersStore.users)
const mapUsers = computed(() => users.value.map(el => ({...el, ...userSection(el)})))
const displayUsers = computed(() => {
if (!search.value || !(search.value && search.value.trim())) return mapUsers.value
const searchValue = search.value.trim().toLowerCase()
const arrOut = mapUsers.value
.filter(el =>
el.section1.toLowerCase().includes(searchValue) ||
el.section2_1.toLowerCase().includes(searchValue) ||
el.section2_2.toLowerCase().includes(searchValue) ||
el.section3.toLowerCase().includes(searchValue)
)
return arrOut
})
function userSection (user: User) {
const tname = () => {
return user.firstname
? user.lastname
? user.firstname + ' ' + user.lastname
: user.firstname
: user.lastname ?? ''
}
const section1 = user.fullname
? user.fullname
: tname()
const section2_1 = user.fullname
? tname()
: ''
const section2_2 = user.username ?? ''
const section3 = (
user.company_id
? user.company_id + ((user.role || user.department ) ? ', ' :'')
: ''
) + (
user.department
? user.department + ' '
: ''
) + (
user.role ?? ''
)
return {
section1,
section2_1, section2_2,
section3
}
}
async function goUserInfo (id: number) {
await router.push({ name: 'user_info', params: { userId: id }})
}
</script>
<style>
</style>