mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-12 18:22:16 +01:00
add e2e tests for employee restrictions
This commit is contained in:
@@ -67,6 +67,12 @@ const { groupByOptions, getNameForReportingRowEntry, emptyPlaceholder } = report
|
||||
|
||||
const organization = inject<ComputedRef<Organization>>('organization');
|
||||
|
||||
const showBillableRate = computed(() => {
|
||||
return !!(
|
||||
getCurrentRole() !== 'employee' || organization?.value?.employees_can_see_billable_rates
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure sub-group falls back when it collides with group
|
||||
watch(
|
||||
group,
|
||||
@@ -280,12 +286,14 @@ const tableData = computed(() => {
|
||||
groupByOptions.filter((el) => el.value !== group)
|
||||
"></ReportingGroupBySelect>
|
||||
</div>
|
||||
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
|
||||
<div
|
||||
class="grid items-center"
|
||||
:style="`grid-template-columns: 1fr 100px ${showBillableRate ? '150px' : ''}`">
|
||||
<div
|
||||
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-secondary [&>*]:pb-1.5 [&>*]:pt-1 text-text-tertiary text-sm">
|
||||
<div class="pl-6">Name</div>
|
||||
<div class="text-right">Duration</div>
|
||||
<div class="text-right pr-6">Cost</div>
|
||||
<div v-if="showBillableRate" class="text-right pr-6">Cost</div>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
@@ -297,6 +305,7 @@ const tableData = computed(() => {
|
||||
:key="entry.description ?? 'none'"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:type="aggregatedTableTimeEntries.grouped_type"
|
||||
:show-cost="showBillableRate"
|
||||
:entry="entry"></ReportingRow>
|
||||
<div class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
||||
<div class="flex items-center pl-6 font-medium">
|
||||
@@ -311,7 +320,9 @@ const tableData = computed(() => {
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="justify-end pr-6 flex items-center font-medium">
|
||||
<div
|
||||
v-if="showBillableRate"
|
||||
class="justify-end pr-6 flex items-center font-medium">
|
||||
{{
|
||||
aggregatedTableTimeEntries.cost
|
||||
? formatCents(
|
||||
@@ -328,7 +339,8 @@ const tableData = computed(() => {
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="chart flex flex-col items-center justify-center py-12 col-span-3">
|
||||
class="chart flex flex-col items-center justify-center py-12"
|
||||
:class="showBillableRate ? 'col-span-3' : 'col-span-2'">
|
||||
<p class="text-lg text-text-primary font-medium">No time entries found</p>
|
||||
<p>Try to change the filters and time range</p>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ const props = defineProps<{
|
||||
entry: AggregatedGroupedData;
|
||||
indent?: boolean;
|
||||
currency: string;
|
||||
showCost?: boolean;
|
||||
}>();
|
||||
|
||||
const expanded = ref(false);
|
||||
@@ -50,7 +51,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="justify-end pr-6 flex items-center">
|
||||
<div v-if="showCost" class="justify-end pr-6 flex items-center">
|
||||
{{
|
||||
entry.cost
|
||||
? formatCents(
|
||||
@@ -66,12 +67,14 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
||||
</div>
|
||||
<div
|
||||
v-if="expanded && entry.grouped_data"
|
||||
class="col-span-3 grid bg-tertiary"
|
||||
style="grid-template-columns: 1fr 150px 150px">
|
||||
:class="showCost ? 'col-span-3' : 'col-span-2'"
|
||||
class="grid bg-tertiary"
|
||||
:style="`grid-template-columns: 1fr 150px ${showCost ? '150px' : ''}`">
|
||||
<ReportingRow
|
||||
v-for="subEntry in entry.grouped_data"
|
||||
:key="subEntry.description ?? 'none'"
|
||||
:currency="props.currency"
|
||||
:show-cost="showCost"
|
||||
indent
|
||||
:entry="subEntry"></ReportingRow>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||
import { canViewReport } from '@/utils/permissions';
|
||||
import { computed } from 'vue';
|
||||
defineProps<{
|
||||
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
active: 'reporting' | 'detailed' | 'shared';
|
||||
}>();
|
||||
|
||||
const showSharedReports = computed(() => canViewReport());
|
||||
|
||||
const tabs = computed(() => {
|
||||
const items = [
|
||||
{ value: 'reporting', label: 'Overview', href: route('reporting') },
|
||||
{ value: 'detailed', label: 'Detailed', href: route('reporting.detailed') },
|
||||
];
|
||||
if (showSharedReports.value) {
|
||||
items.push({
|
||||
value: 'shared',
|
||||
label: 'Shared',
|
||||
href: route('reporting.shared'),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
function hrefForTab(value: string) {
|
||||
return tabs.value.find((tab) => tab.value === value)?.href;
|
||||
}
|
||||
|
||||
function onTabChange(value: string | number) {
|
||||
const href = hrefForTab(String(value));
|
||||
if (href) {
|
||||
router.visit(href);
|
||||
}
|
||||
}
|
||||
|
||||
function onTabHover(value: string) {
|
||||
const href = hrefForTab(value);
|
||||
if (href) {
|
||||
router.prefetch(href, {}, { cacheFor: '1m' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabBar :model-value="active">
|
||||
<TabBarItem value="reporting" @click="router.visit(route('reporting'))"
|
||||
>Overview</TabBarItem
|
||||
>
|
||||
<TabBarItem value="detailed" @click="router.visit(route('reporting.detailed'))"
|
||||
>Detailed</TabBarItem
|
||||
>
|
||||
<TabBar :default-value="props.active" @update:model-value="onTabChange">
|
||||
<TabBarItem
|
||||
v-if="showSharedReports"
|
||||
value="shared"
|
||||
@click="router.visit(route('reporting.shared'))"
|
||||
>Shared</TabBarItem
|
||||
>
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:value="tab.value"
|
||||
@mouseenter="onTabHover(tab.value)">
|
||||
{{ tab.label }}
|
||||
</TabBarItem>
|
||||
</TabBar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -14,13 +14,12 @@ 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 { storeToRefs } from 'pinia';
|
||||
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 { getCurrentRole } from '@/utils/useUser';
|
||||
import { useOrganizationStore } from '@/utils/useOrganization';
|
||||
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';
|
||||
@@ -31,7 +30,7 @@ import { NO_CLIENT_ID } from '@/Components/Common/Project/constants';
|
||||
// Fetch data using TanStack Query
|
||||
const { projects } = useProjectsQuery();
|
||||
const { clients } = useClientsQuery();
|
||||
const { organization } = storeToRefs(useOrganizationStore());
|
||||
const { organization } = useOrganizationQuery(getCurrentOrganizationId()!);
|
||||
|
||||
// Table state persisted in localStorage
|
||||
interface ProjectTableState {
|
||||
|
||||
@@ -5,7 +5,6 @@ import SectionBorder from '@/Components/SectionBorder.vue';
|
||||
import UpdateTeamNameForm from '@/Pages/Teams/Partials/UpdateTeamNameForm.vue';
|
||||
import type { Organization } from '@/types/models';
|
||||
import type { Permissions, Role } from '@/types/jetstream';
|
||||
import { canUpdateOrganization } from '@/utils/permissions';
|
||||
import OrganizationBillableRate from '@/Pages/Teams/Partials/OrganizationBillableRate.vue';
|
||||
import OrganizationFormatSettings from '@/Pages/Teams/Partials/OrganizationFormatSettings.vue';
|
||||
import OrganizationTimeEntrySettings from '@/Pages/Teams/Partials/OrganizationTimeEntrySettings.vue';
|
||||
@@ -46,13 +45,13 @@ onMounted(async () => {
|
||||
<UpdateTeamNameForm :team="team" :permissions="permissions" />
|
||||
|
||||
<SectionBorder />
|
||||
<OrganizationBillableRate v-if="canUpdateOrganization()" :team="team" />
|
||||
<OrganizationBillableRate v-if="permissions.canUpdateTeam" :team="team" />
|
||||
<SectionBorder />
|
||||
|
||||
<OrganizationFormatSettings v-if="canUpdateOrganization()" :team="team" />
|
||||
<OrganizationFormatSettings v-if="permissions.canUpdateTeam" :team="team" />
|
||||
<SectionBorder />
|
||||
|
||||
<OrganizationTimeEntrySettings v-if="canUpdateOrganization()" />
|
||||
<OrganizationTimeEntrySettings v-if="permissions.canUpdateTeam" />
|
||||
<SectionBorder />
|
||||
|
||||
<template v-if="permissions.canDeleteTeam && !team.personal_team">
|
||||
|
||||
@@ -39,7 +39,7 @@ const maxWidthClass = computed(() => {
|
||||
<template>
|
||||
<Dialog :open="show" @update:open="close">
|
||||
<DialogContent :class="maxWidthClass">
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user