mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
improve reporting page responsive layout; standardize button sizing;
prevent mobile input zoom; increase CI playwright shards
This commit is contained in:
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
@@ -9,8 +9,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shardIndex: [1, 2, 3, 4]
|
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
shardTotal: [4]
|
shardTotal: [8]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mailpit:
|
mailpit:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 1 : 0,
|
retries: process.env.CI ? 1 : 0,
|
||||||
/* Run tests in parallel */
|
/* Run tests in parallel */
|
||||||
workers: 4,
|
workers: process.env.CI ? 2 : 4,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: process.env.CI ? 'blob' : 'html',
|
reporter: process.env.CI ? 'blob' : 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChartBarIcon } from '@heroicons/vue/20/solid';
|
import { ChartBarIcon, ArrowDownTrayIcon, EllipsisVerticalIcon, LockClosedIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import { SaveIcon } from 'lucide-vue-next';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import {
|
import {
|
||||||
formatHumanReadableDuration,
|
formatHumanReadableDuration,
|
||||||
@@ -8,17 +9,25 @@ import {
|
|||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
||||||
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
|
|
||||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
|
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
|
||||||
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
|
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
|
||||||
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||||
import ReportingExportModal from '@/Components/Common/Reporting/ReportingExportModal.vue';
|
import ReportingExportModal from '@/Components/Common/Reporting/ReportingExportModal.vue';
|
||||||
import ReportSaveButton from '@/Components/Common/Report/ReportSaveButton.vue';
|
|
||||||
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
||||||
import ReportingFilterBar from '@/Components/Common/Reporting/ReportingFilterBar.vue';
|
import ReportingFilterBar from '@/Components/Common/Reporting/ReportingFilterBar.vue';
|
||||||
|
import { SecondaryButton } from '@/packages/ui/src';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/Components/ui/dropdown-menu';
|
||||||
|
import ReportCreateModal from '@/Components/Common/Report/ReportCreateModal.vue';
|
||||||
|
import UpgradeModal from '@/Components/Common/UpgradeModal.vue';
|
||||||
|
import { canCreateReports } from '@/utils/permissions';
|
||||||
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
import { computed, type ComputedRef, inject, ref, watch } from 'vue';
|
import { computed, type ComputedRef, inject, ref, watch } from 'vue';
|
||||||
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
||||||
import {
|
import {
|
||||||
@@ -182,6 +191,28 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
const { projects } = useProjectsQuery();
|
const { projects } = useProjectsQuery();
|
||||||
const showExportModal = ref(false);
|
const showExportModal = ref(false);
|
||||||
const exportUrl = ref<string | null>(null);
|
const exportUrl = ref<string | null>(null);
|
||||||
|
const showCreateReportModal = ref(false);
|
||||||
|
const showPremiumModal = ref(false);
|
||||||
|
const exportLoading = ref(false);
|
||||||
|
|
||||||
|
function triggerExport(format: ExportFormat) {
|
||||||
|
if (format === 'pdf' && !isAllowedToPerformPremiumAction()) {
|
||||||
|
showPremiumModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exportLoading.value = true;
|
||||||
|
downloadExport(format).finally(() => {
|
||||||
|
exportLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSaveReportClick() {
|
||||||
|
if (isAllowedToPerformPremiumAction()) {
|
||||||
|
showCreateReportModal.value = true;
|
||||||
|
} else {
|
||||||
|
showPremiumModal.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const groupedPieChartData = computed(() => {
|
const groupedPieChartData = computed(() => {
|
||||||
return (
|
return (
|
||||||
@@ -240,16 +271,90 @@ const tableData = computed(() => {
|
|||||||
<ReportingExportModal
|
<ReportingExportModal
|
||||||
v-model:show="showExportModal"
|
v-model:show="showExportModal"
|
||||||
:export-url="exportUrl"></ReportingExportModal>
|
:export-url="exportUrl"></ReportingExportModal>
|
||||||
|
<ReportCreateModal
|
||||||
|
v-model:show="showCreateReportModal"
|
||||||
|
:properties="reportProperties"></ReportCreateModal>
|
||||||
|
<UpgradeModal v-model:show="showPremiumModal">
|
||||||
|
This feature is only available in solidtime Professional.
|
||||||
|
</UpgradeModal>
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
class="h-14 sm:h-16 border-b border-default-background-separator flex flex-wrap gap-y-3 justify-between items-center">
|
||||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||||
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
||||||
<ReportingTabNavbar active="reporting"></ReportingTabNavbar>
|
<ReportingTabNavbar
|
||||||
|
active="reporting"
|
||||||
|
class="hidden sm:flex"></ReportingTabNavbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="hidden sm:flex space-x-2">
|
||||||
<ReportingExportButton :download="downloadExport"></ReportingExportButton>
|
<DropdownMenu>
|
||||||
<ReportSaveButton :report-properties="reportProperties"></ReportSaveButton>
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SecondaryButton :icon="ArrowDownTrayIcon" :loading="exportLoading">
|
||||||
|
Export
|
||||||
|
</SecondaryButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem @click="triggerExport('pdf')">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>Export as PDF</span>
|
||||||
|
<LockClosedIcon
|
||||||
|
v-if="!isAllowedToPerformPremiumAction()"
|
||||||
|
class="w-3.5 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('xlsx')">
|
||||||
|
Export as Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('csv')">
|
||||||
|
Export as CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('ods')">
|
||||||
|
Export as ODS
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<SecondaryButton
|
||||||
|
v-if="canCreateReports()"
|
||||||
|
:icon="SaveIcon"
|
||||||
|
@click="onSaveReportClick">
|
||||||
|
Save Report
|
||||||
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child class="sm:hidden">
|
||||||
|
<button
|
||||||
|
class="p-1.5 rounded-lg border border-border-tertiary text-text-secondary hover:text-text-primary hover:bg-secondary transition"
|
||||||
|
aria-label="More options">
|
||||||
|
<EllipsisVerticalIcon class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem @click="triggerExport('pdf')">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>Export as PDF</span>
|
||||||
|
<LockClosedIcon
|
||||||
|
v-if="!isAllowedToPerformPremiumAction()"
|
||||||
|
class="w-3.5 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('xlsx')">
|
||||||
|
Export as Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('csv')">
|
||||||
|
Export as CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('ods')">
|
||||||
|
Export as ODS
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-if="canCreateReports()"
|
||||||
|
@click="onSaveReportClick">
|
||||||
|
Save Report
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MainContainer>
|
||||||
|
<MainContainer class="sm:hidden py-2 border-b border-default-background-separator">
|
||||||
|
<ReportingTabNavbar active="reporting"></ReportingTabNavbar>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<ReportingFilterBar
|
<ReportingFilterBar
|
||||||
v-model:selected-members="selectedMembers"
|
v-model:selected-members="selectedMembers"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const props = defineProps<{
|
|||||||
:icon="icon"
|
:icon="icon"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'rounded-md px-2 sm:px-3 py-1 border sm:py-1.5 text-xs sm:text-sm font-medium text-text-tertiary hover:text-text-primary focus-visible:outline-none data-[state=active]:bg-tab-background data-[state=active]:border-input-border data-[state=active]:text-text-primary border-tab-border',
|
'rounded-md px-2 sm:px-3 border py-1.5 text-xs sm:text-sm font-medium text-text-tertiary hover:text-text-primary focus-visible:outline-none data-[state=active]:bg-tab-background data-[state=active]:border-input-border data-[state=active]:text-text-primary border-tab-border',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ window.addEventListener('dashboard:refresh', () => {
|
|||||||
filteredLatestTasks.length === 4 ? 'last:border-0' : ''
|
filteredLatestTasks.length === 4 ? 'last:border-0' : ''
|
||||||
"></RecentlyTrackedTasksCardEntry>
|
"></RecentlyTrackedTasksCardEntry>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center flex flex-1 justify-center items-center">
|
<div v-else class="text-center flex flex-1 justify-center items-center py-5">
|
||||||
<div>
|
<div>
|
||||||
<PlusCircleIcon class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
<PlusCircleIcon class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
||||||
<h3 class="text-text-primary font-semibold text-sm">No recent time entries</h3>
|
<h3 class="text-text-primary font-semibold text-sm">No recent time entries</h3>
|
||||||
<p class="pb-5 text-sm">Start tracking your time!</p>
|
<p class="text-sm">Start tracking your time!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const { data: latestTeamActivity, isLoading } = useQuery({
|
|||||||
<div v-else class="text-center text-gray-500 py-8">No team activity found</div>
|
<div v-else class="text-center text-gray-500 py-8">No team activity found</div>
|
||||||
<div
|
<div
|
||||||
v-if="latestTeamActivity && latestTeamActivity.length <= 1"
|
v-if="latestTeamActivity && latestTeamActivity.length <= 1"
|
||||||
|
:class="latestTeamActivity?.length === 1 ? 'pb-5' : 'py-5'"
|
||||||
class="text-center flex flex-1 justify-center items-center">
|
class="text-center flex flex-1 justify-center items-center">
|
||||||
<div>
|
<div>
|
||||||
<UserGroupIcon class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
|
<UserGroupIcon class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const props = defineProps<{
|
|||||||
data-slot="input"
|
data-slot="input"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex h-9 w-full rounded-md border border-input-border bg-input-background px-3 py-1 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-9 w-full rounded-md border border-input-border bg-input-background px-3 py-1 text-base sm:text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
" />
|
" />
|
||||||
|
|||||||
@@ -9,7 +9,17 @@ import {
|
|||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ChevronDoubleRightIcon,
|
ChevronDoubleRightIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
|
EllipsisVerticalIcon,
|
||||||
|
ArrowDownTrayIcon,
|
||||||
|
LockClosedIcon,
|
||||||
} from '@heroicons/vue/20/solid';
|
} from '@heroicons/vue/20/solid';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/Components/ui/dropdown-menu';
|
||||||
|
import { SecondaryButton } from '@/packages/ui/src';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -46,7 +56,7 @@ import {
|
|||||||
import { useQueryClient } from '@tanstack/vue-query';
|
import { useQueryClient } from '@tanstack/vue-query';
|
||||||
import { getCurrentOrganizationId, getCurrentMembershipId } from '@/utils/useUser';
|
import { getCurrentOrganizationId, getCurrentMembershipId } from '@/utils/useUser';
|
||||||
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
||||||
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
|
import UpgradeModal from '@/Components/Common/UpgradeModal.vue';
|
||||||
import type { ExportFormat } from '@/types/reporting';
|
import type { ExportFormat } from '@/types/reporting';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
|
||||||
@@ -160,6 +170,19 @@ const selectedTimeEntries = ref<TimeEntry[]>([]);
|
|||||||
|
|
||||||
const showExportModal = ref(false);
|
const showExportModal = ref(false);
|
||||||
const exportUrl = ref<string | null>(null);
|
const exportUrl = ref<string | null>(null);
|
||||||
|
const showPremiumModal = ref(false);
|
||||||
|
const exportLoading = ref(false);
|
||||||
|
|
||||||
|
function triggerExport(format: ExportFormat) {
|
||||||
|
if (format === 'pdf' && !isAllowedToPerformPremiumAction()) {
|
||||||
|
showPremiumModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exportLoading.value = true;
|
||||||
|
downloadExport(format).finally(() => {
|
||||||
|
exportLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function createTag(name: string) {
|
async function createTag(name: string) {
|
||||||
return await useTagsStore().createTag(name);
|
return await useTagsStore().createTag(name);
|
||||||
@@ -236,13 +259,76 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
<ReportingExportModal
|
<ReportingExportModal
|
||||||
v-model:show="showExportModal"
|
v-model:show="showExportModal"
|
||||||
:export-url="exportUrl"></ReportingExportModal>
|
:export-url="exportUrl"></ReportingExportModal>
|
||||||
|
<UpgradeModal v-model:show="showPremiumModal">
|
||||||
|
<strong>PDF Reports</strong> are only available in solidtime Professional.
|
||||||
|
</UpgradeModal>
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
class="h-14 sm:h-16 border-b border-default-background-separator flex flex-wrap gap-y-3 justify-between items-center">
|
||||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||||
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
||||||
<ReportingTabNavbar active="detailed"></ReportingTabNavbar>
|
<ReportingTabNavbar
|
||||||
|
active="detailed"
|
||||||
|
class="hidden sm:flex"></ReportingTabNavbar>
|
||||||
</div>
|
</div>
|
||||||
<ReportingExportButton :download="downloadExport"></ReportingExportButton>
|
<div class="hidden sm:block">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SecondaryButton :icon="ArrowDownTrayIcon" :loading="exportLoading">
|
||||||
|
Export
|
||||||
|
</SecondaryButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem @click="triggerExport('pdf')">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>Export as PDF</span>
|
||||||
|
<LockClosedIcon
|
||||||
|
v-if="!isAllowedToPerformPremiumAction()"
|
||||||
|
class="w-3.5 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('xlsx')">
|
||||||
|
Export as Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('csv')">
|
||||||
|
Export as CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('ods')">
|
||||||
|
Export as ODS
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child class="sm:hidden">
|
||||||
|
<button
|
||||||
|
class="p-1.5 rounded-lg border border-border-tertiary text-text-secondary hover:text-text-primary hover:bg-secondary transition"
|
||||||
|
aria-label="More options">
|
||||||
|
<EllipsisVerticalIcon class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem @click="triggerExport('pdf')">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span>Export as PDF</span>
|
||||||
|
<LockClosedIcon
|
||||||
|
v-if="!isAllowedToPerformPremiumAction()"
|
||||||
|
class="w-3.5 text-text-tertiary" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('xlsx')">
|
||||||
|
Export as Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('csv')">
|
||||||
|
Export as CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="triggerExport('ods')">
|
||||||
|
Export as ODS
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MainContainer>
|
||||||
|
<MainContainer class="sm:hidden py-2 border-b border-default-background-separator">
|
||||||
|
<ReportingTabNavbar active="detailed"></ReportingTabNavbar>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
|
|
||||||
<ReportingFilterBar
|
<ReportingFilterBar
|
||||||
|
|||||||
@@ -69,12 +69,17 @@ watch(currentPage, () => {
|
|||||||
<template>
|
<template>
|
||||||
<AppLayout title="Reporting" data-testid="reporting_view" class="overflow-hidden">
|
<AppLayout title="Reporting" data-testid="reporting_view" class="overflow-hidden">
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-3 sm:py-5 min-h-[79px] border-b border-default-background-separator flex justify-between items-center">
|
class="h-14 sm:h-16 border-b border-default-background-separator flex flex-wrap gap-y-3 justify-between items-center">
|
||||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||||
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
<PageTitle :icon="ChartBarIcon" title="Reporting"></PageTitle>
|
||||||
<ReportingTabNavbar active="shared"></ReportingTabNavbar>
|
<ReportingTabNavbar
|
||||||
|
active="shared"
|
||||||
|
class="hidden sm:flex"></ReportingTabNavbar>
|
||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
|
<MainContainer class="sm:hidden py-2 border-b border-default-background-separator">
|
||||||
|
<ReportingTabNavbar active="shared"></ReportingTabNavbar>
|
||||||
|
</MainContainer>
|
||||||
|
|
||||||
<div v-if="!isAllowedToPerformPremiumAction()">
|
<div v-if="!isAllowedToPerformPremiumAction()">
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const props = withDefaults(
|
|||||||
<button
|
<button
|
||||||
:type="type"
|
:type="type"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="inline-flex items-center px-3 py-2 bg-button-primary-background border border-button-primary-border rounded-md font-medium text-xs sm:text-sm text-button-primary-text hover:bg-button-primary-background-hover active:bg-button-primary-background-hover focus:outline-none focus-visible:ring-2 focus-visible:border-transparent focus-visible:ring-ring transition ease-in-out duration-150">
|
class="inline-flex items-center h-9 px-3 text-sm bg-button-primary-background border border-button-primary-border rounded-md font-medium text-button-primary-text hover:bg-button-primary-background-hover active:bg-button-primary-background-hover focus:outline-none focus-visible:ring-2 focus-visible:border-transparent focus-visible:ring-ring transition ease-in-out duration-150">
|
||||||
<span :class="twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')">
|
<span :class="twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')">
|
||||||
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
||||||
<component
|
<component
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
small: 'text-xs px-2.5 py-1.5',
|
small: 'text-xs px-2.5 py-1.5',
|
||||||
base: 'text-xs sm:text-sm px-3 py-2',
|
base: 'h-9 px-3 text-sm',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const model = defineModel();
|
|||||||
v-model="model"
|
v-model="model"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'h-9 px-3 py-1 text-sm border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
|
'h-9 px-3 py-1 text-base sm:text-sm border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ useSelectEvents(
|
|||||||
:enable-estimated-time="enableEstimatedTime"
|
:enable-estimated-time="enableEstimatedTime"
|
||||||
@changed="updateProject"></TimeTrackerProjectTaskDropdown>
|
@changed="updateProject"></TimeTrackerProjectTaskDropdown>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center @2xl:space-x-2 px-2 @2xl:px-4">
|
<div class="flex items-center space-x-1 @2xl:space-x-2 px-2 @2xl:px-4 shrink-0">
|
||||||
<TimeTrackerTagDropdown
|
<TimeTrackerTagDropdown
|
||||||
v-model="currentTimeEntry.tags"
|
v-model="currentTimeEntry.tags"
|
||||||
:create-tag
|
:create-tag
|
||||||
|
|||||||
@@ -498,16 +498,16 @@ const showCreateProject = ref(false);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="projects.length === 0 && canCreateProject">
|
<template v-if="projects.length === 0 && canCreateProject">
|
||||||
<Button
|
<Button
|
||||||
:variant="props.variant"
|
:variant="props.variant"
|
||||||
:size="props.size"
|
:size="props.size"
|
||||||
:class="twMerge('w-full justify-start', props.class)"
|
:class="twMerge('w-full justify-start', props.class)"
|
||||||
@click="showCreateProject = true">
|
@click="showCreateProject = true">
|
||||||
<PlusIcon class="w-4" />
|
<PlusIcon class="w-4" />
|
||||||
<span>Add new project</span>
|
<span class="truncate">Add new project</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</template>
|
||||||
<Dropdown v-else v-model="open" :close-on-content-click="false" :align="props.align">
|
<Dropdown v-else v-model="open" :close-on-content-click="false" :align="props.align">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user