before store

This commit is contained in:
2025-06-26 11:06:48 +03:00
parent 1c732e16dd
commit 34baeb40e3
59 changed files with 3180 additions and 2149 deletions

View File

@@ -6,22 +6,11 @@
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
{{ $t('error404') }}
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script setup lang="ts">
//
</script>

View File

@@ -40,15 +40,7 @@
>
<template #default>
<div class="flex column items-center">
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'">
<q-badge
color="brand" align="top"
rounded floating
style="font-style: normal;"
>
{{ currentProject?.[tab.name as keyof typeof currentProject] ?? 0 }}
</q-badge>
</q-icon>
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'"/>
<div
class="text-caption flex justify-center"
:style="{ width: (baseTabWidth - 32) + 'px'}"
@@ -71,14 +63,11 @@
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { useProjectsStore } from 'stores/projects'
import { useRoute } from 'vue-router'
import projectPageHeader from 'pages/main/HeaderPage.vue'
const route = useRoute()
const projectsStore = useProjectsStore()
const currentProject = computed(() => projectsStore.currentProjectId)
const tabs = ref([
{name: 'files', label: 'main__files', icon: 'mdi-file-multiple-outline', to: { name: 'files'}, width: 0 },

View File

@@ -1,18 +1,10 @@
<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>
<meeting-block
v-model="newMeeting"
title="meeting_create__title_card"
btnText="meeting_create__btn"
@update="addMeeting"
/>
</template>
<script setup lang="ts">
@@ -25,19 +17,19 @@
const router = useRouter()
const meetingsStore = useMeetingsStore()
const meetingMod = ref(<MeetingParams>{
const newMeeting = ref(<MeetingParams>{
name: '',
description: '',
place: '',
meet_date: Date.now(),
chat_attach: null,
meet_date: (Date.now() + 1000*60*60*24) / 1000, // fix incorrect start date and time -> move 24h
chat_id: null,
participants: [],
files: [],
is_cancel: false
})
async function addCompany (data: MeetingParams) {
await meetingsStore.add(data)
async function addMeeting (newFiles: File[]) {
await meetingsStore.add(newMeeting.value, newFiles)
router.go(-1)
}

View File

@@ -1,236 +1,45 @@
<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>
<meeting-block
v-if="meetingMod"
v-model="meetingMod"
title="meeting_edit__title_card"
btnText="meeting_edit__btn"
@update="updateMeeting"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import meetingBlock from 'components/meetingBlock.vue'
import { useMeetingsStore } from 'stores/meetings'
import { parseIntString } from 'helpers/helpers'
import type { MeetingParams } from 'types/Meeting'
const router = useRouter()
const router = useRouter()
const route = useRoute()
const meetingsStore = useMeetingsStore()
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 meetingId = computed(() => parseIntString(route.params.meetingId))
const meetingMod = ref<MeetingParams | null>(null)
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: [] },
])
if (meetingsStore.isInit) {
meetingMod.value = meetingId.value
? { ...meetingsStore.meetingById(meetingId.value) } as MeetingParams
: null
}
watch(() => meetingsStore.isInit, (isInit) => {
if (isInit && meetingId.value && !meetingMod.value) {
meetingMod.value = { ...meetingsStore.meetingById(meetingId.value) as MeetingParams }
}
})
async function createMeeting () {
await router.push({ name: 'meetings' })
}
const updateMeeting = async (newFiles: File[]) => {
if (!meetingId.value || !meetingMod.value) return
await meetingsStore.update(meetingId.value, meetingMod.value)
await meetingsStore.updateParticipants(meetingId.value, meetingMod.value.participants)
if (newFiles.length !== 0) await meetingsStore.attachFiles(meetingId.value, newFiles)
router.go(-1)
}
</script>
<style scoped>
.bold-input::v-deep .q-field__native {
font-weight: bold;
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
</style>

View File

@@ -1,195 +1,133 @@
<template>
<pn-page-card>
<pn-page-card v-if="meeting">
<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>
{{ $t('meeting_view__title_card') }}
<q-btn
v-if="meeting.is_editable"
@click = "editMeeting"
flat round
icon="edit"
/>
</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 class="flex column items-center q-px-md q-gutter-y-md w100">
<div class="flex w100 items-center ellipsis text-caption text-grey no-wrap ellipsis q-mt-none" v-if="meeting.chat_id">
<div class="flex items-center justify-start w100 no-wrap">
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
<span class="ellipsis">{{ chatsStore.chatById(meeting.chat_id)?.name }}</span>
</div>
<q-select
v-if="companies"
v-model="task.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('meeting_add__attached_chat')"
<q-btn
@click="goChat"
flat rounded no-caps dense
color="primary"
>
<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 class="flex no-wrap items-center text-caption">
<span class="q-ml-sm">{{$t('meeting_view__go_to_chat')}}</span>
<q-icon name="mdi-chevron-right"/>
</div>
</q-btn>
</div>
<div class="w100" style="border-bottom: 1px solid #eee;">
<div class="flex w100 justify-between text-h6 text-bold">
<div>
<span class="text-caption text-grey">
{{ date.formatDate(meeting.meet_date * 1000, 'ddd') }}
</span>
{{ date.formatDate(meeting.meet_date * 1000, 'DD MMMM') }}
<span v-if="showYear">
{{ date.formatDate(meeting.meet_date * 1000, 'YYYY') }}
</span>
</div>
<div>
{{ date.formatDate(meeting.meet_date * 1000, 'HH:mm') }}
</div>
</div>
<div class="flex w100 items-center text-caption text-grey no-wrap" v-if="meeting.place">
<q-icon name="mdi-map-marker-outline" class="q-mr-xs"/>
<span>{{ meeting.place }}</span>
</div>
</div>
<div
class="text-bold flex w100 self-start"
style="white-space: pre-line"
>
{{ meeting.name }}
</div>
<div
v-if="meeting.description"
class="flex w100 self-start q-mt-none"
style="white-space: pre-line"
>
{{ meeting.description }}
</div>
<div
v-if="meeting.files&&meeting.files.length!==0"
class="flex w100 no-wrap items-start"
>
<div class="flex column w100">
<div
v-for="(item, idx) in meeting.files"
:key="idx"
class="flex items-center text-caption"
>
<q-icon
:name="getFileIcon(item).icon"
:style="{color: getFileIcon(item).color}"
size="sm"
class="q-mr-sm"
/>
{{ filesStore.fileById(item)?.filename }}
</div>
</div>
</div>
<div
v-if="meeting.participants && meeting.participants.length!==0"
class="flex w100 no-wrap items-center"
>
<div class="flex w100 column">
<div class="flex row no-wrap q-mb-sm">
<q-btn
flat dense rounded
@click="isParticipantsShow = !isParticipantsShow"
>
<pn-chain-avatar :users="meeting.participants" :overlap=10 :maxDisplayUsers=3 />
<q-icon
name="mdi-chevron-down"
size="sm"
color="grey"
style="transition: transform 0.3s ease;"
:class="{ 'rotate-180': isParticipantsShow}"
/>
</q-btn>
</div>
<transition
appear
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut"
>
<div class="flex column w100 q-ml-xs" v-if="isParticipantsShow">
<div
v-for="(item, idx) in meeting.participants"
:key="idx"
class="flex items-center q-py-xs cursor-pointer"
@click="goUserInfo(item)"
>
<pn-auto-avatar
:img="usersStore.userById(item)?.photo"
:name="usersStore.userNameById(item)"
size="sm"
class="q-mr-sm"
/>
{{ usersStore.userNameById(item) }}
</div>
</div>
</transition>
</div>
</div>
</div>
</pn-scroll-list>
@@ -197,40 +135,74 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { computed, inject, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useMeetingsStore } from 'stores/meetings'
import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
import { useFilesStore } from 'stores/files'
import { fileIcon } from 'helpers/files-functions'
import { parseIntString } from 'helpers/helpers'
import { date } from 'quasar'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const router = useRouter()
const router = useRouter()
const route = useRoute()
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 meetingsStore = useMeetingsStore()
const meetingId = parseIntString(route.params.meetingId)
const meeting = computed(() => meetingId ? meetingsStore.meetingById(meetingId) : null)
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: [] },
])
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const filesStore = useFilesStore()
const isParticipantsShow = ref<boolean>(false)
async function editMeeting () {
await router.push({ name: 'meeting_edit', params: { meetingId }})
}
const showYear = computed(() =>
meeting.value && meeting.value.meet_date &&
(date.formatDate(Date.now(), 'YYYY') !==
date.formatDate(meeting.value.meet_date * 1000, 'YYYY'))
)
function goChat () {
if (meeting.value && meeting.value.chat_id) {
const chat = chatsStore.chatById(meeting.value.chat_id)
if (chat) {
const invite = chat.invite_link
tg.openTelegramLink(invite)
}
}
}
function getFileIcon (id: number) {
const file = filesStore.fileById(id)
return file
? fileIcon(file.filename)
: { color: '', icon: ''}
}
async function goUserInfo (id: number) {
await router.push({ name: 'user_info', params: { userId: id }})
}
async function createMeeting () {
await router.push({ name: 'meetings' })
}
</script>
<style scoped>
.bold-input::v-deep .q-field__native {
font-weight: bold;
.fix-bottom-padding.q-field--with-bottom {
padding-bottom: 0 !important
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
.file-input-fix :deep(.q-field__prepend) {
height: auto !important;
}
</style>

View File

@@ -1,236 +1,39 @@
<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>
<task-block
v-model="newTask"
title="task_create__title_card"
btnText="task_create__btn"
@update="addTask"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import taskBlock from 'components/taskBlock.vue'
import { useTasksStore } from 'stores/tasks'
import type { TaskParams } from 'types/Task'
const router = useRouter()
const router = useRouter()
const tasksStore = useTasksStore()
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;
const newTask = ref(<TaskParams>{
name: '',
description: '',
assigned_to: null,
priority: 0,
status: 1,
plan_date: (Date.now() + 1000*60*60*24) / 1000, // fix incorrect start date and time -> move 24h
observers: [],
files: [],
chat_id: null,
close_files: [],
close_comment: '',
})
async function addTask () {
await tasksStore.add(newTask.value)
router.go(-1)
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
</style>
</script>

View File

@@ -0,0 +1,34 @@
<template>
<task-block
v-model="taskMod"
title="task_edit__title_card"
btnText="task_edit__btn"
@update=updateTask()
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import taskBlock from 'components/taskBlock.vue'
import { useTasksStore } from 'stores/tasks'
import type { TaskParams } from 'types/Task'
import { parseIntString } from 'src/helpers/helpers'
const router = useRouter()
const route = useRoute()
const tasksStore = useTasksStore()
const taskId = parseIntString(route.params.taskId)
const initialTask = taskId && tasksStore.taskById(taskId)
const taskMod = ref({
...initialTask
} as TaskParams)
async function updateTask () {
if (!taskId || !initialTask) return
await tasksStore.update(taskId, taskMod.value)
router.go(-1)
}
</script>

View File

@@ -1,93 +1,327 @@
<template>
<pn-page-card>
<pn-page-card v-if="task">
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('settings__title') }}
{{ $t('task_view__title_card') }}
<q-btn
v-if="task.is_editable"
@click = "editTask"
flat round
icon="edit"
/>
</template>
<pn-scroll-list>
<div class="flex column items-center q-pa-md q-gutter-y-md w100">
<div class="flex w100 items-center ellipsis text-caption text-grey no-wrap ellipsis q-mt-none" v-if="task.chat_id">
<div class="flex items-center justify-start w100 no-wrap">
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
<span class="ellipsis">{{ chatsStore.chatById(task.chat_id)?.name }}</span>
</div>
<q-btn
@click="goChat"
flat rounded no-caps dense
color="primary"
>
<div class="flex no-wrap items-center text-caption">
<span class="q-ml-sm">{{$t('meeting_view__go_to_chat')}}</span>
<q-icon name="mdi-chevron-right"/>
</div>
</q-btn>
</div>
<div class="w100" style="border-bottom: 1px solid #eee;">
<div class="flex w100 justify-between text-caption">
<div class="flex items-baseline text-bold">
<q-icon name="mdi-clock-outline" class="q-mr-xs"/>
<span class="q-mr-sm text-weight-regular">
{{ date.formatDate(task.plan_date * 1000, 'ddd')}}
</span>
<div class="text-body1 flex items-end">
{{ date.formatDate(task.plan_date * 1000, !showYear ? 'DD MMMM' : 'DD MMMM YYYY') }}
<span class="q-ml-xs">
{{ date.formatDate(task.plan_date * 1000, 'HH:mm') }}
</span>
</div>
</div>
<pn-task-priority-icon v-if="task.priority" :priority="task.priority" label/>
</div>
<div class="flex items-center q-my-sm">
<div
v-if="task.created_by !== usersStore.myId.id"
class="flex items-center"
>
<pn-auto-avatar
:img="usersStore.userById(task.created_by)?.photo"
:name="usersStore.userNameById(task.created_by)"
size="sm"
class="q-mr-xs"
/>
<span>
{{ usersStore.userNameById(task.created_by) }}
</span>
</div>
<span v-else>{{$t('task_item__from_me')}}</span>
<q-icon name="mdi-chevron-right" size="xs" class="text-grey"/>
<div
v-if="task.assigned_to && task.assigned_to !== usersStore.myId.id"
class="flex items-center"
>
<pn-auto-avatar
:img="usersStore.userById(task.assigned_to)?.photo"
:name="usersStore.userNameById(task.assigned_to)"
size="sm"
class="q-mr-xs"
/>
{{ usersStore.userNameById(task.assigned_to) }}
</div>
<span v-else-if="task.created_by !== task.assigned_to">{{$t('task_item__to_me')}}</span>
<span v-else>{{$t('task_item__to_me_from_me')}}</span>
</div>
</div>
<div class="text-bold flex w100 self-start">
{{ task.name }}
</div>
<div
v-if="task.description"
class="flex w100 self-start"
style="white-space: pre-line"
>
{{ task.description }}
</div>
<div
v-if="task.files&&task.files.length!==0"
class="flex w100 no-wrap items-start"
>
<q-icon name="attach_file" class="q-mr-xs"/>
<div class="flex column w100">
<span
v-for="(item, idx) in task.files"
:key="idx"
class="text-caption q-pl-sm"
style="border-left: solid 1px grey"
>
{{ filesStore.fileById(item)?.filename }}
</span>
</div>
</div>
<div
v-if="task.files&&task.files.length!==0"
class="flex w100 no-wrap items-start"
>
<div class="flex column w100">
<div
v-for="(item, idx) in task.files"
:key="idx"
class="flex items-center text-caption"
>
<q-icon
:name="getFileIcon(item).icon"
:style="{color: getFileIcon(item).color}"
size="sm"
class="q-mr-sm"
/>
{{ filesStore.fileById(item)?.filename }}
</div>
</div>
</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>
<template #footer>
<div class="flex no-wrap w100 justify-center q-gutter-x-md q-mt-md q-mb-xs">
<q-btn
:label="$t('task_view__btn_cancel_task')"
outline
color="grey"
rounded
class="w50"
@click="showCloseTaskDialog = true; typeCloseTaskDialog = 'cancel'"
/>
<q-btn
:label="$t('task_view__btn_close_task')"
color="primary"
rounded
class="w50"
@click="showCloseTaskDialog = true; typeCloseTaskDialog = 'done'"
/>
</div>
</template>
</pn-page-card>
<pn-bottom-sheet-dialog
:title="$t(closeTaskDialog.title)"
v-model="showCloseTaskDialog"
>
<div class="w100 q-gutter-y-lg flex column">
<q-input
v-model="closeComment.comment"
dense
filled
autogrow
autofocus
:rules="[rules.comment]"
class = "w100 fix-bottom-padding q-pt-sm"
no-error-icon
label-slot
>
<template #label>
{{ $t( closeTaskDialog.commentLabel) }}
<span class="text-red">*</span>
</template>
</q-input>
<pn-file-uploader
v-model:exist-files ="closeComment.existFiles"
v-model:new-files ="closeComment.newFiles"
:label="$t('task_view__dialog_task_files')"
class="q-pt-sm"
/>
</div>
<template #footer>
<q-btn
rounded
class="w100"
:color="closeTaskDialog.color"
@click="closeTask"
>
{{$t(closeTaskDialog.btnText)}}
</q-btn>
</template>
</pn-bottom-sheet-dialog>
</template>
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useTasksStore } from 'stores/tasks'
import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
import { useFilesStore } from 'stores/files'
import { fileIcon } from 'helpers/files-functions'
import { parseIntString } from 'helpers/helpers'
import { useI18n } from 'vue-i18n'
import { watch, ref } from 'vue'
import { useTextSizeStore } from 'src/stores/textSize'
import { date } from 'quasar'
import type { WebApp } from '@twa-dev/types'
import type { TaskParams } from 'types/Task'
const tg = inject('tg') as WebApp
const { t } = useI18n()
const { locale } = useI18n()
const savedLocale = localStorage.getItem('locale') || 'en-US'
locale.value = savedLocale
const router = useRouter()
const route = useRoute()
const localeOptions = ref([
{ value: 'en-US', label: 'English' },
{ value: 'ru-RU', label: 'Русский' }
])
const tasksStore = useTasksStore()
const taskId = parseIntString(route.params.taskId)
const task = computed(() => taskId && tasksStore.taskById(taskId))
watch(locale, (newLocale) => {
localStorage.setItem('locale', newLocale)
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const filesStore = useFilesStore()
async function editTask () {
await router.push({ name: 'task_edit', params: { taskId }})
}
const showYear = computed(() =>
task.value && task.value.plan_date &&
(date.formatDate(Date.now(), 'YYYY') !==
date.formatDate(task.value.plan_date * 1000, 'YYYY'))
)
function goChat () {
if (task.value && task.value.chat_id) {
const chat = chatsStore.chatById(task.value.chat_id)
if (chat) {
const invite = chat.invite_link
tg.openTelegramLink(invite)
}
}
}
function getFileIcon (id: number) {
const file = filesStore.fileById(id)
return file
? fileIcon(file.filename)
: { color: '', icon: ''}
}
const showCloseTaskDialog = ref(false)
const typeCloseTaskDialog = ref<'done'| 'cancel' | null>(null)
const closeTaskDialog = computed(() =>{
switch (typeCloseTaskDialog.value) {
case 'done':
return {
title: 'task_view__dialog_task_done_title',
commentLabel: 'task_view__dialog_task_done_comment',
btnText: 'task_view__dialog_task_done_btn',
color: 'primary'
}
case 'cancel':
return {
title: 'task_view__dialog_task_cancel_title',
commentLabel: 'task_view__dialog_task_cancel_comment',
btnText: 'task_view__dialog_task_cancel_btn',
color: 'negative'
}
case null:
default:
return {
title: '',
commentLabel: '',
btnText: '',
color: ''
}
}
})
const textSizeStore = useTextSizeStore()
const currentTextSize = textSizeStore.currentFontSize
const maxTextSize = textSizeStore.maxFontSize
const minTextSize = textSizeStore.minFontSize
interface Comment {
comment: string
existFiles: File[]
newFiles: File[]
}
const defaultCloseComment = {
comment: '',
existFiles: [],
newFiles: []
}
const closeComment = ref<Comment>({ ...defaultCloseComment })
function closeTask () {
showCloseTaskDialog.value = false
}
watch (showCloseTaskDialog, () => closeComment.value = { ...defaultCloseComment })
const rulesErrorMessage = {
comment: t('task_view__error_comment')
}
const rules = {
comment: (val: TaskParams['close_comment']) => !!val?.trim() || rulesErrorMessage['comment']
}
</script>
<style scoped>
.fix-input-right :deep(.q-field__native) {
justify-content: end;
.fix-bottom-padding.q-field--with-bottom {
padding-bottom: 0 !important
}
.file-input-fix :deep(.q-field__append) {
height: auto !important;
}
.file-input-fix :deep(.q-field__prepend) {
height: auto !important;
}
.fix-card-width {
width: var(--body-width) !important;
}
</style>

View File

@@ -2,37 +2,35 @@
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('user_card__title') }}
</div>
{{ $t('user_card__title') }}
</div>
</template>
<pn-scroll-list>
<div
v-if="user"
class="flex column items-center q-pa-lg"
class="flex column items-center q-pa-md q-pb-sm"
>
<q-avatar size="100px">
<q-img v-if="user.photo" :src="user.photo"/>
<pn-auto-avatar v-else :name="tname"/>
</q-avatar>
<pn-auto-avatar
:img="user.photo"
:name="tname"
size="100px"
class="q-mr-sm"
/>
<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"
@click="item.f()"
round
:icon="item.icon"
:disable = "item.disable"
color="primary"
/>
</div>
@@ -44,6 +42,7 @@
v-show="item"
:model-value="displayUser[key]"
dense
readonly
filled
class="w100"
:label = "$t('user_card__' + key)"
@@ -60,21 +59,19 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed, inject } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { computed, inject } from 'vue'
import { useRoute } from 'vue-router'
import { useUsersStore } from 'stores/users'
import type { User } from 'types/User'
import { parseIntString } from 'boot/helpers'
import { parseIntString } from 'src/helpers/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 user = computed(() => userId && usersStore.userById(userId))
const tname = computed(() => {
return (!user.value)
@@ -101,23 +98,11 @@
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' })
}
const userActions = computed(() =>[
{ id: 0, icon: 'mdi-chat-outline', f: messageUser, disable: !(user.value && user.value.username) },
{ id: 1, icon: 'mdi-phone-outline', f: callUser, disable: tg.platform !== 'ios' && tg.platform !== 'android' },
{ id: 2, icon: 'mdi-share-variant-outline', f: shareUser, disable: false }
])
function generateVCard () {
if (!user.value) return ''
@@ -137,7 +122,8 @@
}
function messageUser () {
const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? user.value.telegram_id))
console.log((!user.value ? '' : (user.value.username)))
const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? undefined))
if (tg?.platform !== 'unknown') {
tg?.openLink(telegramUrl, { try_instant_view: true })
@@ -147,21 +133,37 @@
}
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')
}
if (user.value) window.open('tel:' + normalizePhoneString(user.value.phone))
}
function shareUser () {
function shareUser () {
tg.openTelegramLink('tg://resolve?domain=ready_or_not_2025_bot&startchannel&admin=')
// не работает, отправляет текст
const tgShareUrl = 'https://t.me/share/url?url= &text=' + encodeURIComponent(generateVCard())
tg.openTelegramLink(tgShareUrl)
// const tgShareUrl = 'https://t.me/share/url?url= &text=' + encodeURIComponent(generateVCard())
// tg.openTelegramLink(tgShareUrl)
}
function normalizePhoneString (input: string): string | null {
const phonePattern = /\+?[\d\s\-()]+/g
const matches = input.match(phonePattern) || []
for (const match of matches) {
let cleaned = match.replace(/[^\d+]/g, '')
const digitCount = cleaned.replace(/\D/g, '').length
if (digitCount < 5) continue
if (cleaned.startsWith('+')) {
cleaned = '+' + cleaned.slice(1).replace(/\D/g, '')
} else {
cleaned = cleaned.replace(/\D/g, '')
}
return cleaned
}
return null
}
</script>
<style>

View File

@@ -27,10 +27,11 @@
@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>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
/>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">
@@ -64,12 +65,12 @@
const search = ref('')
const chatsStore = useChatsStore()
const chats = computed(() => chatsStore.chats)
const chats = chatsStore.getChats
const displayChats = computed(() => {
if (!search.value || !(search.value && search.value.trim())) return chats.value
if (!search.value || !(search.value && search.value.trim())) return chats
const searchValue = search.value.trim().toLowerCase()
const arrOut = chats.value
const arrOut = chats
.filter(el =>
(el.name && el.name.toLowerCase().includes(searchValue)) ||
(el.description && el.description.toLowerCase().includes(searchValue))

View File

@@ -73,20 +73,29 @@
</div>
</q-slide-transition>
</template>
<q-list separator>
<q-item
v-for="item in displayFiles"
:key="item.id"
:clickable="false"
@click="goFile(item)"
v-ripple
clickable
>
<q-item-section avatar class="items-center" >
<q-avatar rounded>
<q-avatar>
<q-icon
:name="fileIcon(item.filename).icon"
:style="{color: fileIcon(item.filename).color}"
size="md"
/>
</q-avatar>
<span
class="text-caption text-grey"
style="line-height: 0.25rem;"
>
{{ parseFileName(item.filename).ext }}
</span>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">
@@ -97,25 +106,25 @@
<div class="second-line-item flex no-wrap items-center q-mr-sm">
<q-icon
:name="item.parent_type === 0
? 'mdi-message-outline'
? 'mdi-chat-outline'
: item.parent_type === 1
? 'mdi-clipboard-outline'
: 'mdi-calendar-month-outline'
"
class="q-mr-none"
class="q-mr-xs"
/>
<span class="ellipsis">{{ fileFrom(item.chat_id) }}</span>
<div class="ellipsis">{{ fileFrom(item) }}</div>
</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>
<q-icon name="mdi-account-outline" class="q-mr-xs"/>
<div class="ellipsis">{{ usersStore.userNameById(item.published_by) }}</div>
</div>
</div>
</q-item-label>
<q-item-label caption lines="1">
<div class = "flex justify-between items-center">
<span>{{ fileSize(item.size) }}</span>
<div class = "flex justify-between items-center text-caption">
<span>{{ fileSize(item.size, $t) }}</span>
<span>{{ fileDate(item.published) }}</span>
</div>
</q-item-label>
@@ -124,122 +133,126 @@
</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>
<pn-bottom-sheet-dialog
title="files__filters"
v-model="showFiltersDialog"
>
<template #btnSlot>
<div>
<q-btn
v-if="!checkFiltersSelect"
@click="resetFilters"
flat
no-caps
dense
color="grey-6"
>
{{ $t('files__filters_reset')}}
</q-btn>
</div>
</template>
<template #footer>
<q-btn
rounded
class="w100"
color="primary"
@click="showFiltersDialog = false"
>
{{$t('files__filters_continue')}}
</q-btn>
</template>
<div class="q-pl-sm text-bold text-caption">
{{ $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="filters.fileExt.includes(fileIcon(item).type) ? 'active' : 'inactive'"
size="md"
class="q-pa-sm"
/>
</div>
</div>
<div class="q-pl-sm q-mt-md text-bold text-caption">
{{ $t('files__filters_source') }}
</div>
<div class="flex column">
<div
v-for="(item,idx) in fileSourceOptions"
:key="idx"
>
<q-checkbox
v-model="filters.fileSource"
:val="item.value"
>
{{ $t(item.label) }}
</q-checkbox>
</div>
</div>
<div class="q-pl-sm q-mt-md text-bold text-caption">
{{ $t('files__filters_by') }}
</div>
<q-select
v-model="filters.fileBy"
:options="fileByOptions"
dense
filled
class="w100 q-pt-sm"
option-value="id"
option-label="displayName"
emit-value
map-options
use-chips
multiple
/>
<div class="q-pl-sm q-mt-md text-bold text-caption">
{{ $t('files__filters_size') }}
</div>
<div class="flex column">
<div
v-for="(item,idx) in fileSizeOptions"
:key="idx"
>
<q-checkbox
v-model="filters.fileSize"
:val="item.value"
:label = "$t(item.label)"
/>
</div>
</div>
</pn-bottom-sheet-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, inject } from 'vue'
import { useFilesStore } from 'stores/files'
import { useI18n } from 'vue-i18n'
import { useUsersStore } from 'stores/users'
import { useChatsStore } from 'stores/chats'
import { date } from 'quasar'
import type { File } from 'types/File'
const { t }= useI18n()
import { parseFileName, fileIcon, fileSize } from 'helpers/files-functions'
import { useRouter } from 'vue-router'
import type { FileLink } from 'types/FileLink'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const search = ref('')
const filesStore = useFilesStore()
const chatsStore = useChatsStore()
const files = computed(() => filesStore.files)
const files = filesStore.getFiles
const showCalendar = ref<boolean>(false)
const showFiltersDialog = ref(false)
@@ -248,42 +261,43 @@
const displayFiles = computed(() => {
return files.value
return files
.filter(searchFiles)
.filter(fileExt)
.filter(fileSource)
.filter(fileSize)
.filter(fileBy)
.filter(checkDateInterval)
function searchFiles (el: File) {
function searchFiles (el: FileLink) {
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) {
function checkDateInterval (el: FileLink) {
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)
return (from < el.published * 1000) && ( to >= el.published * 1000)
}
function fileExt (el: File) {
if (fileExtFilters.value.length === 0) return true
function fileExt (el: FileLink) {
if (filters.value.fileExt.length === 0) return true
const type = fileIcon(el.filename).type
return fileExtFilters.value.includes(type)
return filters.value.fileExt.includes(type)
}
function fileSource (el: File) {
if (fileSourceFilters.value.length === 0) return true
return fileSourceFilters.value.includes(el.parent_type)
function fileSource (el: FileLink) {
if (filters.value.fileSource.length === 0) return true
return filters.value.fileSource.includes(el.parent_type)
}
function fileSize (el: File) {
if (fileSizeFilters.value.length === 0) return true
function fileSize (el: FileLink) {
if (filters.value.fileSize.length === 0) return true
const fileSize = el.size
const sortedFilters = [...fileSizeFilters.value].sort((a, b) => a - b)
const sortedFilters = [...filters.value.fileSize].sort((a, b) => a - b)
const ranges = fileSizeOptions.map((option, index) => ({
min: index === 0 ? 0 : fileSizeOptions[index - 1]!.value,
@@ -297,186 +311,89 @@
(range.max === Infinity ? true : fileSize <= range.max))
)
}
function fileBy (el: FileLink) {
if (filters.value.fileBy.length === 0) return true
return filters.value.fileBy.includes(el.published_by)
}
})
const fileExtFilters = ref<string[]>([])
interface Filters {
fileSource: number[]
fileSize: number[]
fileBy: number[]
fileExt: string[]
}
const defaultFilters = {
fileSource: [],
fileSize: [],
fileBy: [],
fileExt: []
}
const filters = ref<Filters>({ ...defaultFilters })
const checkFiltersSelect = computed(() => (
Object.values(filters.value).every(el => el.length === 0)
))
function resetFilters() {
(Object.keys(filters.value) as (keyof Filters)[]).forEach(key => filters.value[key] = [])
}
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)
if (filters.value.fileExt.includes(select)) {
const idx = filters.value.fileExt.indexOf(select)
filters.value.fileExt.splice(idx, 1)
} else filters.value.fileExt.push(select)
}
const fileSourceFilters = ref<number[]>([])
const fileSourceOptions = [
{ id: 1, icon: 'mdi-message-outline', value: 0, label: 'files__filters_source_chats' },
{ id: 1, icon: 'mdi-chat-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)
}
const usersStore = useUsersStore()
const users = computed(() => usersStore.users)
const fileByOptions = computed(() => {
return users.value
.map(el => ({ ...el, displayName: usersStore.userNameById(el.id) }))
})
function fileBy (value: number): string {
return String(value)
}
function fileSize (value: number): string {
if (value === 0) return '-'
const filesDates = computed(() => displayFiles.value.map(el => date.formatDate(el.published * 1000, 'YYYY/MM/DD')))
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)
function fileFrom (file: FileLink) {
const result = (value / Math.pow(1024, index)).toFixed(2)
return `${result} ${t(units[index]??'')}`
if (file.parent_type === 0) {
const chat = chatsStore.chatById(file.chat_id)
if (chat) return chat.name
}
return String(file.chat_id)
}
function fileDate (value: number): string {
return date.formatDate(value, 'DD-MM-YY')
function fileDate (time: number) {
return date.formatDate(time*1000, 'DD.MM.YYYY HH:mm')
}
const dialogSectionHeight = ref<number>(0)
interface sizeParams {
height: number,
width: number
}
function onResize (size: sizeParams) {
dialogSectionHeight.value = size.height
const router = useRouter()
async function goFile(file: FileLink) {
if (file.parent_type === 0) tg.openTelegramLink('https://t.me/c/' + file.telegram_chat_id + '/' + file.message_id)
if (file.parent_type === 1) await router.push({ name: 'task_info', params: { taskId: file.parent_id }})
if (file.parent_type === 2) await router.push({ name: 'meeting_info', params: { meetingId: file.parent_id }})
}
</script>

View File

@@ -27,20 +27,23 @@
@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>
<pn-auto-avatar
:img="project.logo"
:name="project.name"
type="rounded"
/>
<div class="flex column text-white fit">
<div
class="text-h6 q-pl-sm"
:style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{ project.name }}
</div>
<div class="text-caption q-pl-sm" :style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }">
<div
class="text-caption q-pl-sm"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{ project.description }}
</div>
</div>
@@ -49,14 +52,19 @@
</div>
<q-btn
v-if="projects.length > 1"
@click="drawer=!drawer"
:class="drawer ? 'text-grey' : 'text-white'"
flat
round
icon="mdi-briefcase-outline"
size="md"
no-caps
dense
class="q-ml-xl"
/>
>
<span class="flex items-center">
{{ $t('header__projects') }}
<q-icon name="mdi-chevron-right"/>
</span>
</q-btn>
</div>
<q-drawer
v-model="drawer"
@@ -89,14 +97,21 @@
@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>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
/>
</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-label
class="text-caption"
lines="2"
style="white-space: pre-line"
>
{{item.description}}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
@@ -120,7 +135,7 @@
const currentProjectId = computed(() => projectsStore.currentProjectId)
const projects = computed(() => projectsStore.projects)
const projects = projectsStore.getProjects
const project = computed(() => {
const currentProject = currentProjectId.value && projectsStore.projectById(currentProjectId.value)
@@ -144,9 +159,9 @@
const router = useRouter()
const route = useRoute()
function changeProject(id: number) {
async function changeProject(id: number) {
drawer.value = false
router.push({ name: route.name, params: { id }})
await router.push({ name: route.name, params: { id }})
}
interface sizeParams {

View File

@@ -99,7 +99,7 @@
backgroundColor:
item.is_cancel
? '#999'
: item.meet_date < Date.now()
: item.meet_date * 1000 < Date.now()
? '#eee'
: 'inherit',
border: item.is_cancel ? 'solid 1px #999' : 'inherit'
@@ -131,12 +131,12 @@
<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') }}
{{ $t('meeting_page__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-item-label lines="1" class="text-caption flex items-center" v-if="item.place">
<q-icon name="mdi-map-marker-outline" class="q-mr-none"/>
<span class="ellipsis">{{ item.place }}</span>
</q-item-label>
@@ -144,13 +144,13 @@
<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"/>
<q-icon name="mdi-account-tie-outline" class="q-mr-xs"/>
<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 class="second-line-item flex no-wrap items-center" v-if="item.chat_id">
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
<span class="ellipsis">{{ chatsStore.chatById(item.chat_id)?.name }}</span>
</div>
</div>
</q-item-label>
@@ -162,7 +162,7 @@
<span>{{ item.participants?.length }}</span>
</span>
<span class="text-caption flex items-center" v-if="item.files && item.length !== 0">
<span class="text-caption flex items-center" v-if="item.files && item.files.length !== 0">
<q-icon name="mdi-paperclip" color="grey" />
<span>{{ item.files.length }}</span>
</span>
@@ -194,90 +194,29 @@
</q-page-sticky>
</div>
<q-dialog
<pn-small-dialog
v-model="showDialogDeleteMeeting"
icon="mdi-calendar-remove-outline"
color="negative"
title="meeting_page__dialog_cancel_title"
mainBtnLabel="meeting_page__dialog_cancel_ok"
auxBtnLabel="meeting_page__dialog_cancel_delete"
@clickMainBtn="onConfirmCancel()"
@clickAuxBtn="onConfirmDelete()"
@close="onCancel()"
@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
<pn-small-dialog
v-model="showDialogRestoreMeeting"
icon="mdi-calendar-refresh-outline"
color="green"
title="meeting_page__dialog_restore_title"
mainBtnLabel="meeting_page__dialog_restore_ok"
@clickMainBtn="onConfirmRestore()"
@close="onCancel()"
@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">
@@ -315,12 +254,12 @@
time: string
}
const meetings = computed(() => meetingsStore.meetings)
const meetingsDates = computed(() => displayMeetingsAll.value.map(el => date.formatDate(el.meet_date, 'YYYY/MM/DD')))
const meetings = meetingsStore.getMeetings
const meetingsDates = computed(() => displayMeetingsAll.value.map(el => date.formatDate(el.meet_date * 1000, 'YYYY/MM/DD')))
const displayMeetingsAll = computed(() => {
return meetings.value
return meetings
.filter(searchMeetings)
.filter(checkDateInterval)
.sort(sortByTime)
@@ -339,7 +278,7 @@
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)
return (from < el.meet_date * 1000) && ( to >= el.meet_date * 1000)
}
function sortByTime (a: Meeting, b: Meeting) {
@@ -349,11 +288,11 @@
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')
monthYear: date.formatDate(el.meet_date * 1000, 'MMMM YYYY'),
day: date.formatDate(el.meet_date * 1000, 'D'),
isWeekend: (date.formatDate(el.meet_date * 1000, 'E') === '6'|| date.formatDate(el.meet_date * 1000, 'E') === '7'),
dayOfWeek: date.formatDate(el.meet_date * 1000, 'ddd'),
time: date.formatDate(el.meet_date * 1000, 'HH:mm')
}
}
})
@@ -361,7 +300,7 @@
const displayTime = ref(Date.now())
const currentMeetings = computed(() =>
displayMeetingsAll.value.filter(el => el.meet_date >= displayTime.value)
displayMeetingsAll.value.filter(el => el.meet_date * 1000 >= displayTime.value)
)
const countPreviousMeetings = computed(() =>
@@ -369,13 +308,13 @@
)
const showReset = computed(() =>
currentMeetings.value.length !== displayMeetingsAll.value.filter(el => el.meet_date >= Date.now()).length
currentMeetings.value.length !== displayMeetingsAll.value.filter(el => el.meet_date * 1000 >= 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()
displayTime.value = (displayMeetingsAll.value[newIndex] && displayMeetingsAll.value[newIndex]?.meet_date * 1000) || Date.now()
}
function resetDisplayPreviousMeetings () {
@@ -398,7 +337,7 @@
let currentGroup: ResultItem
currentMeetings.value.forEach((el: enrichMeeting) => {
const month = date.formatDate(el.meet_date, 'MMMM YYYY')
const month = date.formatDate(el.meet_date * 1000, 'MMMM YYYY')
if (month !== currentMonth) {
currentMonth = month

View File

@@ -11,8 +11,18 @@
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
@@ -27,11 +37,11 @@
</q-input>
<q-btn
@click="showFilters = true"
@click="showFiltersDialog = true"
icon="mdi-filter-outline"
dense round flat
size="lg"
:color="showFilters ? 'primary' : 'grey'"
:color="showFiltersDialog ? 'primary' : 'grey'"
class="q-mr-xs"
>
<div>
@@ -41,38 +51,69 @@
floating
transparent
style="position: relative; top: -16px; margin-left: -12px"
:style="{ opacity: selectedFilters.length !== 0 ? 0.8 : 0 }"
: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="date"
v-model="datesRange"
range
flat
:events="tasksDates"
event-color="brand"
today-btn
minimal
dense
/>
</div>
</q-slide-transition>
</template>
<q-list bordered separator>
<q-item
v-for="item in displayTasks"
:key="item.id"
clickable v-ripple
<div class="w100 flex justify-end q-px-md">
<q-btn flat dense no-caps @click="showArchiveTasks=!showArchiveTasks">
<span class="text-caption text-grey">
{{
$t(!showArchiveTasks ? 'tasks__show_archive' : 'tasks__hide_archive') +
(hiddenTask !== 0 ? ' (+' + hiddenTask +')' : '')
}}
</span>
</q-btn>
</div>
<template v-for="item in displayTasks" :key="item.id">
<q-slide-item
@right="handleSlideRight($event, item.id)"
@left="handleSlideLeft($event, item.id)"
clickable
v-ripple
@click="goTask(item.id)"
right-color="red"
left-color="green"
>
<task-item :item/>
</q-item>
</q-list>
<div class="q-py-lg"/> <!-- fix hide scroll area by tabs -->
<template #right v-if="item.status !== 6">
<q-icon size="lg" name="mdi-clipboard-remove-outline"/>
</template>
<template #left v-if="item.status === 6">
<q-icon size="lg" name="mdi-clipboard-play-outline"/>
</template>
<q-item
:key="item.id"
:style = "{
backgroundColor:
item.status === 6
? '#999'
: 'inherit',
border: item.status === 6 ? 'solid 1px #999' : 'inherit'
}"
>
<task-item :item/>
</q-item>
</q-slide-item>
</template>
</pn-scroll-list>
<q-page-sticky
position="bottom-right"
:offset="[0, 18]"
@@ -93,124 +134,225 @@
</q-page-sticky>
</div>
<q-dialog
v-model="showFilters"
position="bottom"
<pn-small-dialog
v-model="showDialogDeleteTask"
icon="mdi-clipboard-remove-outline"
color="negative"
title="tasks__dialog_cancel_title"
mainBtnLabel="tasks__dialog_cancel_ok"
auxBtnLabel="tasks__dialog_cancel_delete"
@clickMainBtn="onConfirmCancel()"
@clickAuxBtn="onConfirmDelete()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
<pn-small-dialog
v-model="showDialogRestoreTask"
icon="mdi-clipboard-play-outline"
color="green"
title="tasks__dialog_restore_title"
mainBtnLabel="tasks__dialog_restore_ok"
@clickMainBtn="onConfirmRestore()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
<pn-bottom-sheet-dialog
title="tasks__filters"
v-model="showFiltersDialog"
>
<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"
/>
<template #btnSlot>
<div>
<q-btn
v-if="!checkFiltersSelect"
@click="resetFilters"
flat
no-caps
dense
color="grey-6"
>
{{ $t('tasks__filters_reset')}}
</q-btn>
</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>
</template>
<template #footer>
<q-btn
rounded
class="w100"
color="primary"
@click="showFiltersDialog = false"
>
{{$t('tasks__filters_continue')}}
</q-btn>
</template>
<div class="q-pl-sm text-bold text-caption">
{{ $t('tasks__filters_by_participant') }}
</div>
</q-dialog>
<div class="flex column">
<div
v-for="(item,idx) in taskParticipantsOptions"
:key="idx"
>
<q-checkbox
v-model="filters.byParticipants"
:val="item.value"
>
{{ $t(item.label) }}
</q-checkbox>
</div>
</div>
<div class="q-pl-sm q-mt-md text-bold text-caption">
{{ $t('tasks__filters_by_priority') }}
</div>
<div class="flex column">
<div
v-for="(item,idx) in taskPriorityOptions"
:key="idx"
>
<q-checkbox
v-model="filters.byPriority"
:val="item.value"
>
{{ $t(item.label) }}
</q-checkbox>
</div>
</div>
</pn-bottom-sheet-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
import { useTasksStore } from 'stores/tasks'
import { useUsersStore } from 'stores/users'
import { useRouter } from 'vue-router'
import taskItem from 'components/taskItem.vue'
import type { Task } from 'types/Task'
import { date } from 'quasar'
const search = ref('')
const showCalendar = ref<boolean>(false)
const date = ref(Date.now())
const datesRange = ref<null | { from: string, to: string }>(null)
const deleteTaskId = ref<number | undefined>(undefined)
const restoreTaskId = ref<number | undefined>(undefined)
const showDialogDeleteTask = ref<boolean>(false)
const showDialogRestoreTask = ref<boolean>(false)
const showFiltersDialog = ref(false)
const showArchiveTasks = ref(false)
const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false)
const tasksStore = useTasksStore()
const usersStore = useUsersStore()
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))
)
interface SlideEvent {
reset: () => void
}
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))
const tasks = tasksStore.getTasks
const tasksDates = computed(() => tasks.map(el => date.formatDate(el.plan_date * 1000, 'YYYY/MM/DD')))
const displayTasks = computed(() => {
return filteredTasks.value.filter(archiveTasks)
function archiveTasks (el: Task) {
if (showArchiveTasks.value) return true
return (
el.close_date < Date.now() - 7 * 24 * 60 * 60 * 1000 // показыать закрытые менее недели назад
)
}
return filteredTasks
})
const hiddenTask = computed(() => filteredTasks.value.length - displayTasks.value.length)
const filteredTasks = computed(() => {
return tasks
.filter(searchTasks)
.filter(checkDateInterval)
.filter(byParticipants)
.filter(byPriority)
function searchTasks (el: Task) {
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: Task) {
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.plan_date * 1000) && ( to >= el.plan_date * 1000)
}
function byParticipants(task: Task): boolean {
const selected = filters.value.byParticipants
if (selected.length === 0) return true
const userId = usersStore.myId.id
return selected.some(opt => {
switch (opt) {
case 1: return task.assigned_to === userId
case 2: return task.created_by === userId
case 3: return task.observers.includes(userId)
case 4: return task.assigned_to !== userId
&& task.created_by !== userId
&& !task.observers.includes(userId)
default: return false
}
})
}
function byPriority (el: Task) {
if (filters.value.byPriority.length === 0) return true
return filters.value.byPriority.includes(el.priority)
}
})
interface Filters {
byParticipants: number[]
byPriority: number[]
}
const defaultFilters = {
byParticipants: [],
byPriority: []
}
const filters = ref<Filters>({ ...defaultFilters })
const checkFiltersSelect = computed(() => (
Object.values(filters.value).every(el => el.length === 0)
))
function resetFilters() {
(Object.keys(filters.value) as (keyof Filters)[]).forEach(key => filters.value[key] = [])
}
const taskParticipantsOptions = [
{ id: 1, value: 1, label: 'tasks__filters_to_me' },
{ id: 2, value: 2, label: 'tasks__filters_from_me' },
{ id: 3, value: 3, label: 'tasks__filters_observers' },
{ id: 4, value: 4, label: 'tasks__filters_not_involved' }
]
const taskPriorityOptions = [
{ id: 0, value: 0, label: 'tasks__filters_priority_normal' },
{ id: 1, value: 1, label: 'tasks__filters_priority_important' },
{ id: 2, value: 2, label: 'tasks__filters_priority_critical' }
]
async function goTask (taskId: number) {
await new Promise(resolve => setTimeout(resolve, 300))
await router.push({ name: 'task_info', params: { taskId }})
}
@@ -218,6 +360,57 @@
await router.push({ name: 'task_add'})
}
function handleSlideRight (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogDeleteTask.value = true
deleteTaskId.value = id
}
function handleSlideLeft (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogRestoreTask.value = true
restoreTaskId.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 (deleteTaskId.value) {
await tasksStore.remove(deleteTaskId.value)
}
currentSlideEvent.value = null
}
async function onConfirmCancel() {
closedByUserAction.value = true
if (deleteTaskId.value) {
await tasksStore.setCancelStatus(deleteTaskId.value)
}
currentSlideEvent.value = null
}
async function onConfirmRestore() {
closedByUserAction.value = true
if (restoreTaskId.value) {
await tasksStore.setRestoreStatus(restoreTaskId.value)
}
currentSlideEvent.value = null
}
// fix fab jumping
const showFab = ref(false)
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
@@ -245,8 +438,12 @@
<style scoped lang="scss">
/* fix mini border after slide */
:deep(.q-slide-item__right)
{
:deep(.q-slide-item__right) {
align-self: center;
height: 98%;
}
:deep(.q-slide-item__left) {
align-self: center;
height: 98%;
}
@@ -260,10 +457,4 @@
.fix-calendar :deep(.q-date__calendar-days-container) {
min-height: auto;
}
.filter-panel {
max-height: 70vh;
overflow: auto;
}
</style>

View File

@@ -27,26 +27,26 @@
@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>
<pn-auto-avatar
:img="item.photo"
:name="item.section1"
/>
</q-item-section>
<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>