v1
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
44
src/pages/MeetingAddPage.vue
Normal file
44
src/pages/MeetingAddPage.vue
Normal 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>
|
||||
236
src/pages/MeetingEditPage.vue
Normal file
236
src/pages/MeetingEditPage.vue
Normal 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>
|
||||
236
src/pages/MeetingInfoPage.vue
Normal file
236
src/pages/MeetingInfoPage.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
236
src/pages/TaskAddPage.vue
Normal 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>
|
||||
93
src/pages/TaskInfoPage.vue
Normal file
93
src/pages/TaskInfoPage.vue
Normal 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>
|
||||
@@ -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
168
src/pages/UserInfoPage.vue
Normal 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>
|
||||
87
src/pages/main/ChatsPage.vue
Normal file
87
src/pages/main/ChatsPage.vue
Normal 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>
|
||||
504
src/pages/main/FilesPage.vue
Normal file
504
src/pages/main/FilesPage.vue
Normal 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>
|
||||
170
src/pages/main/HeaderPage.vue
Normal file
170
src/pages/main/HeaderPage.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
545
src/pages/main/MeetingsPage.vue
Normal file
545
src/pages/main/MeetingsPage.vue
Normal 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>
|
||||
269
src/pages/main/TasksPage.vue
Normal file
269
src/pages/main/TasksPage.vue
Normal 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>
|
||||
129
src/pages/main/UsersPage.vue
Normal file
129
src/pages/main/UsersPage.vue
Normal 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>
|
||||
Reference in New Issue
Block a user