mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
203 lines
7.8 KiB
Vue
203 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
|
import { FolderIcon, PlusIcon } from '@heroicons/vue/20/solid';
|
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
|
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
|
import { computed } from 'vue';
|
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
|
import { useProjectsStore } from '@/utils/useProjects';
|
|
import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue';
|
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
|
import { canCreateProjects } from '@/utils/permissions';
|
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
|
import { useClientsStore } from '@/utils/useClients';
|
|
import type { CreateClientBody, Client, CreateProjectBody, Project } from '@/packages/api/src';
|
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
|
import { getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
|
|
import { useOrganizationQuery } from '@/utils/useOrganizationQuery';
|
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
|
import { useStorage } from '@vueuse/core';
|
|
import ProjectsFilterDropdown from '@/Components/Common/Project/ProjectsFilterDropdown.vue';
|
|
import ProjectStatusFilterBadge from '@/Components/Common/Project/ProjectStatusFilterBadge.vue';
|
|
import ProjectVisibilityFilterBadge from '@/Components/Common/Project/ProjectVisibilityFilterBadge.vue';
|
|
import ProjectClientFilterBadge from '@/Components/Common/Project/ProjectClientFilterBadge.vue';
|
|
import { NO_CLIENT_ID } from '@/Components/Common/Project/constants';
|
|
import type { SortColumn, SortDirection } from '@/Components/Common/Project/ProjectTable.vue';
|
|
|
|
// Fetch data using TanStack Query
|
|
const { projects } = useProjectsQuery();
|
|
const { clients } = useClientsQuery();
|
|
const { organization } = useOrganizationQuery(getCurrentOrganizationId()!);
|
|
|
|
// Table state persisted in localStorage
|
|
interface ProjectTableState {
|
|
sortColumn: SortColumn;
|
|
sortDirection: SortDirection;
|
|
filters: {
|
|
clientIds: string[];
|
|
status: 'active' | 'archived' | 'all';
|
|
visibility: 'public' | 'private' | 'all';
|
|
};
|
|
}
|
|
|
|
const tableState = useStorage<ProjectTableState>(
|
|
'project-table-state',
|
|
{
|
|
sortColumn: 'name',
|
|
sortDirection: 'asc',
|
|
filters: {
|
|
clientIds: [],
|
|
status: 'all',
|
|
visibility: 'all',
|
|
},
|
|
},
|
|
undefined,
|
|
{
|
|
mergeDefaults: (storage, defaults) => ({
|
|
...defaults,
|
|
...storage,
|
|
filters: { ...defaults.filters, ...storage.filters },
|
|
}),
|
|
}
|
|
);
|
|
|
|
function handleSort(column: SortColumn, direction: SortDirection) {
|
|
tableState.value.sortColumn = column;
|
|
tableState.value.sortDirection = direction;
|
|
}
|
|
|
|
// Filter projects based on current filters
|
|
const filteredProjects = computed(() => {
|
|
return projects.value.filter((project) => {
|
|
// Status filter
|
|
if (tableState.value.filters.status === 'active' && project.is_archived) {
|
|
return false;
|
|
}
|
|
if (tableState.value.filters.status === 'archived' && !project.is_archived) {
|
|
return false;
|
|
}
|
|
|
|
// Visibility filter
|
|
if (tableState.value.filters.visibility === 'public' && !project.is_public) {
|
|
return false;
|
|
}
|
|
if (tableState.value.filters.visibility === 'private' && project.is_public) {
|
|
return false;
|
|
}
|
|
|
|
// Client filter
|
|
const hasClientFilter = tableState.value.filters.clientIds.length > 0;
|
|
if (hasClientFilter) {
|
|
const matchesNoClient =
|
|
tableState.value.filters.clientIds.includes(NO_CLIENT_ID) && !project.client_id;
|
|
const matchesClientId =
|
|
project.client_id && tableState.value.filters.clientIds.includes(project.client_id);
|
|
|
|
if (!matchesNoClient && !matchesClientId) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
});
|
|
|
|
// Helper functions for active filters
|
|
function removeStatusFilter() {
|
|
tableState.value.filters.status = 'all';
|
|
}
|
|
|
|
function removeVisibilityFilter() {
|
|
tableState.value.filters.visibility = 'all';
|
|
}
|
|
|
|
function removeClientFilter() {
|
|
tableState.value.filters.clientIds = [];
|
|
}
|
|
|
|
const showCreateProjectModal = useStorage('project-create-modal-open', false);
|
|
|
|
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
|
|
return await useProjectsStore().createProject(project);
|
|
}
|
|
|
|
async function createClient(client: CreateClientBody): Promise<Client | undefined> {
|
|
return await useClientsStore().createClient(client);
|
|
}
|
|
|
|
const showBillableRate = computed(() => {
|
|
return !!(
|
|
getCurrentRole() !== 'employee' || organization.value?.employees_can_see_billable_rates
|
|
);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout title="Projects" data-testid="projects_view">
|
|
<MainContainer
|
|
class="py-3 sm:pt-5 border-b border-default-background-separator flex justify-between items-center">
|
|
<div class="flex items-center space-x-3 sm:space-x-6">
|
|
<PageTitle :icon="FolderIcon" title="Projects"></PageTitle>
|
|
</div>
|
|
<SecondaryButton
|
|
v-if="canCreateProjects()"
|
|
:icon="PlusIcon"
|
|
@click="showCreateProjectModal = true"
|
|
>Create Project
|
|
</SecondaryButton>
|
|
<ProjectCreateModal
|
|
v-model:show="showCreateProjectModal"
|
|
:create-project
|
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
|
:create-client
|
|
:currency="getOrganizationCurrencyString()"
|
|
:organization-billable-rate="organization?.billable_rate ?? null"
|
|
:clients="clients"
|
|
@submit="createProject"></ProjectCreateModal>
|
|
</MainContainer>
|
|
<MainContainer>
|
|
<div class="flex items-center gap-2 py-1">
|
|
<ProjectsFilterDropdown
|
|
:filters="tableState.filters"
|
|
:clients="clients"
|
|
@update:filters="tableState.filters = $event" />
|
|
|
|
<!-- Active Filters -->
|
|
<ProjectStatusFilterBadge
|
|
v-if="tableState.filters.status !== 'all'"
|
|
data-testid="status-filter-badge"
|
|
:value="tableState.filters.status"
|
|
@remove="removeStatusFilter"
|
|
@update:value="
|
|
tableState.filters.status = $event as 'active' | 'archived' | 'all'
|
|
" />
|
|
|
|
<ProjectVisibilityFilterBadge
|
|
v-if="tableState.filters.visibility !== 'all'"
|
|
data-testid="visibility-filter-badge"
|
|
:value="tableState.filters.visibility"
|
|
@remove="removeVisibilityFilter"
|
|
@update:value="
|
|
tableState.filters.visibility = $event as 'public' | 'private' | 'all'
|
|
" />
|
|
|
|
<ProjectClientFilterBadge
|
|
v-if="tableState.filters.clientIds.length > 0"
|
|
data-testid="client-filter-badge"
|
|
:value="tableState.filters.clientIds"
|
|
:clients="clients"
|
|
@remove="removeClientFilter"
|
|
@update:value="tableState.filters.clientIds = $event as string[]" />
|
|
</div>
|
|
</MainContainer>
|
|
|
|
<ProjectTable
|
|
:show-billable-rate="showBillableRate"
|
|
:projects="filteredProjects"
|
|
:sort-column="tableState.sortColumn"
|
|
:sort-direction="tableState.sortDirection"
|
|
@sort="handleSort"></ProjectTable>
|
|
</AppLayout>
|
|
</template>
|