Files
tgCrewAdmin/src/pages/ProjectsPage.vue
2026-06-10 22:29:34 +03:00

353 lines
10 KiB
Vue

<template>
<pn-page-template @updateScrollPosition="onScroll">
<template #title>
<div class="q-pr-md ellipsis">
{{ $t('projects__title') }}
</div>
<q-btn
@click="goAccount"
flat rounded
no-caps
icon-right="mdi-chevron-right"
align="right"
dense
class="fix-btn ellipsis"
>
<pn-account-block-name/>
</q-btn>
</template>
<template #card-header>
<pn-action-bar
v-if="projects.length !== 0"
v-model="searchProject"
placeholder="projects__search"
:show-calendar-btn="false"
:show-filter-btn="false"
:shadows="isScrolled"
/>
</template>
<q-list v-if="projects.length !== 0">
<template
v-for="item in activeProjects"
:key="item.id"
>
<q-slide-item
@right="handleSlideRight($event, item.id)"
clickable
v-ripple
@click="goProject(item.id)"
right-color="red"
left-color="green"
>
<template #right>
<q-icon size="lg" name="mdi-briefcase-remove-outline"/>
</template>
<q-item>
<q-item-section avatar>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
size="lg"
/>
</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"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{ item.description }}
</q-item-label>
<q-item-label lines="1" caption>
<div class="flex items-center">
<div class="flex items-center">
<q-icon name="mdi-chat-outline"/>
<span>{{ item.chat_count }} </span>
</div>
<div class="flex items-center q-ml-sm">
<q-icon name="mdi-account-outline"/>
<span>{{ item.user_count }}</span>
</div>
</div>
</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>
<q-btn
color="grey"
size="md"
dense flat round
icon="mdi-pencil"
class="q-ml-sm"
@click.stop="editProject(item.id)"
/>
</q-item-label>
</q-item-section>
</q-item>
</q-slide-item>
</template>
</q-list>
<div
v-if="archiveProjects.length !== 0"
class="flex column items-center w100"
>
<q-btn
class="w100 rotate-icon-btn"
color="grey"
flat
no-caps
@click="showArchive = !showArchive"
icon-right="arrow_drop_down"
:class="{ 'rotate-icon': showArchive }"
>
<span class="text-caption">
{{ !showArchive
? $t('projects__show_archive') + ' (' + archiveProjects.length +')'
: $t('projects__hide_archive')
}}
</span>
</q-btn>
<div class="w100" style="overflow: hidden">
<transition
appear
enter-active-class="animated slideInDown"
leave-active-class="animated slideOutUp"
>
<q-list 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>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
size="lg"
/>
</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"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{ item.description }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</transition>
</div>
</div>
<pn-onboard-btn
v-if="projects.length===0 && projectsInit"
icon="mdi-briefcase-plus-outline"
message1="projects__lets_start"
message2="projects__lets_start_description"
@btn-click="createNewProject"
/>
<div
class="column justify-center items-center w100"
style="position: absolute; bottom: 0;"
v-if="!projectsInit"
>
<q-linear-progress indeterminate />
</div>
<template #card-fab>
<transition
appear
enter-active-class="animated zoomIn"
>
<pn-glass-btn
type="fab"
color="primary"
>
<q-btn
@click="createNewProject"
fab unelevated
class="shadow-t"
icon="add"
color="transparent"
/>
</pn-glass-btn>
</transition>
</template>
</pn-page-template>
<pn-small-dialog
v-model="showDialogArchiveProject"
icon="mdi-briefcase-remove-outline"
color="negative"
title="projects__dialog_archive_title"
message1="projects__dialog_archive_message"
message2="projects__dialog_archive_message2"
mainBtnLabel="projects__dialog_archive_ok"
@clickMainBtn="onConfirmArchiveProject()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
<pn-small-dialog
v-model="showDialogRestoreArchive"
icon="mdi-briefcase-upload-outline"
color="green"
title="projects__dialog_restore_title"
message1="projects__dialog_restore_message"
message2="projects__dialog_restore_message2"
mainBtnLabel="projects__dialog_restore_ok"
@clickMainBtn="restoreFromArchive()"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useProjectsStore } from 'stores/projects'
import pnGlassBtn from 'components/pnGlassBtn.vue'
import pnActionBar from 'components/pnActionBar.vue'
import type { Project } from 'types/Project'
const router = useRouter()
const projectsStore = useProjectsStore()
const projects = computed(() => projectsStore.projects)
const projectsInit = computed(() => projectsStore.isInit)
const searchProject = ref('')
const showArchive = ref(false)
const showDialogRestoreArchive = ref(false)
const restoreProjectId = ref<number | undefined> (undefined)
async function goProject (id: number) {
await router.push({ name: 'chats', params: { id }})
}
async function editProject (id: number) {
await router.push({ name: 'project_info', params: { id }})
}
async function goAccount () {
await router.push({ name: 'account' })
}
async function createNewProject () {
await router.push({ name: 'project_add' })
}
function handleArchiveList (id: number) {
showDialogRestoreArchive.value = true
restoreProjectId.value = id
}
async function restoreFromArchive () {
if (restoreProjectId.value) await projectsStore.restore(restoreProjectId.value)
}
const displayProjects = computed(() => {
if (!searchProject.value || !(searchProject.value && searchProject.value.trim())) return projects.value
const searchChatValue = searchProject.value.trim().toLowerCase()
const arrOut = projects.value
.filter((el: Project) =>
el.name.toLowerCase().includes(searchChatValue) ||
el.description && el.description.toLowerCase().includes(searchProject.value)
)
return arrOut
})
const activeProjects = computed(() => {
return displayProjects.value.filter((el: Project) => !el.is_archived)
})
const archiveProjects = computed(() => {
return displayProjects.value.filter((el: Project) => el.is_archived)
})
onMounted(async () => {
if (!projectsStore.isInit) await projectsStore.init()
})
watch(showDialogRestoreArchive, (newD :boolean) => {
if (!newD) restoreProjectId.value = undefined
})
interface SlideEvent {
reset: () => void
}
const currentSlideEvent = ref<SlideEvent | null>(null)
const showDialogArchiveProject = ref<boolean>(false)
const archiveProjectId = ref<number | undefined>(undefined)
const closedByUserAction = ref(false)
function handleSlideRight (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogArchiveProject.value = true
archiveProjectId.value = id
}
function onDialogBeforeHide () {
if (!closedByUserAction.value) {
onCancel()
}
closedByUserAction.value = false
}
async function onConfirmArchiveProject () {
closedByUserAction.value = true
if (archiveProjectId.value) {
await projectsStore.archive(archiveProjectId.value)
}
currentSlideEvent.value = null
}
function onCancel() {
closedByUserAction.value = true
if (currentSlideEvent.value) {
currentSlideEvent.value.reset()
currentSlideEvent.value = null
}
}
const isScrolled = ref(false)
const onScroll = (pos: number) => {
isScrolled.value = pos > 1
}
</script>
<style scoped>
:deep(.q-slide-item__right) {
align-self: center;
height: 98%;
}
.fix-btn :deep(.q-btn__content) {
flex-wrap: nowrap;
}
.rotate-icon-btn {
transition: transform 0.3s;
}
.rotate-icon-btn.rotate-icon :deep(.q-icon) {
transform: rotate(180deg);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.rotate-icon-btn :deep(.q-icon) {
transform: rotate(0deg);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
}
</style>