420 lines
12 KiB
Vue
420 lines
12 KiB
Vue
<template>
|
|
<div class="q-pa-none flex column col-grow no-scroll">
|
|
<pn-scroll-list>
|
|
<template #card-body-header>
|
|
<pn-action-bar
|
|
v-model="search"
|
|
placeholder="tasks__search"
|
|
:calendar-active="showCalendar"
|
|
:filter-active="showFiltersDialog"
|
|
:calendar-badge="!!datesRange"
|
|
:filter-badge="!checkFiltersSelect"
|
|
@toggle-calendar="showCalendar = !showCalendar"
|
|
@open-filters="showFiltersDialog = true"
|
|
/>
|
|
<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="tasksDates"
|
|
event-color="brand"
|
|
today-btn
|
|
minimal
|
|
dense
|
|
/>
|
|
</div>
|
|
</q-slide-transition>
|
|
</template>
|
|
<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>
|
|
<q-list separator>
|
|
<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"
|
|
>
|
|
<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>
|
|
</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="createTask()"
|
|
/>
|
|
</transition>
|
|
</q-page-sticky>
|
|
</div>
|
|
|
|
<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"
|
|
>
|
|
<template #btnSlot>
|
|
<div>
|
|
<q-btn
|
|
v-if="!checkFiltersSelect"
|
|
@click="resetFilters"
|
|
flat
|
|
no-caps
|
|
dense
|
|
color="grey-6"
|
|
>
|
|
{{ $t('tasks__filters_reset')}}
|
|
</q-btn>
|
|
</div>
|
|
</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>
|
|
|
|
<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'
|
|
import pnActionBar from 'components/pnActionBar.vue'
|
|
|
|
const search = ref('')
|
|
const showCalendar = ref<boolean>(false)
|
|
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()
|
|
|
|
interface SlideEvent {
|
|
reset: () => void
|
|
}
|
|
|
|
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 // показыать закрытые менее недели назад
|
|
)
|
|
}
|
|
})
|
|
|
|
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 }})
|
|
}
|
|
|
|
async function createTask () {
|
|
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)
|
|
|
|
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>
|