mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
add format check, update prettier rules, apply rules consistently
This commit is contained in:
@@ -2,11 +2,7 @@
|
||||
import VChart, { THEME_KEY } from 'vue-echarts';
|
||||
import { computed, provide, inject, shallowRef, type ComputedRef } from 'vue';
|
||||
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
|
||||
import {
|
||||
formatDate,
|
||||
formatHumanReadableDuration,
|
||||
formatWeek,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
import { formatDate, formatHumanReadableDuration, formatWeek } from '@/packages/ui/src/utils/time';
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { BarChart } from 'echarts/charts';
|
||||
@@ -19,14 +15,7 @@ import {
|
||||
import type { AggregatedTimeEntries, Organization } from '@/packages/api/src';
|
||||
import { useCssVariable } from '@/utils/useCssVariable';
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
TitleComponent,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
]);
|
||||
use([CanvasRenderer, BarChart, TitleComponent, GridComponent, TooltipComponent, LegendComponent]);
|
||||
|
||||
provide(THEME_KEY, 'dark');
|
||||
|
||||
@@ -174,9 +163,7 @@ const option = computed(() => ({
|
||||
class="chart"
|
||||
:option="option" />
|
||||
<div v-else class="chart flex flex-col items-center justify-center">
|
||||
<p class="text-lg text-text-primary font-semibold">
|
||||
No time entries found
|
||||
</p>
|
||||
<p class="text-lg text-text-primary font-semibold">No time entries found</p>
|
||||
<p>Try to change the filters and time range</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,15 +27,11 @@ function triggerDownload(format: ExportFormat) {
|
||||
<template>
|
||||
<Dropdown align="end">
|
||||
<template #trigger>
|
||||
<SecondaryButton :icon="ArrowDownTrayIcon" :loading>
|
||||
Export
|
||||
</SecondaryButton>
|
||||
<SecondaryButton :icon="ArrowDownTrayIcon" :loading> Export </SecondaryButton>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col space-y-1 p-1.5">
|
||||
<SecondaryButton
|
||||
class="border-0 px-2"
|
||||
@click="triggerDownload('pdf')">
|
||||
<SecondaryButton class="border-0 px-2" @click="triggerDownload('pdf')">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span> Export as PDF </span>
|
||||
<LockClosedIcon
|
||||
@@ -43,27 +39,20 @@ function triggerDownload(format: ExportFormat) {
|
||||
class="w-3.5 text-text-tertiary"></LockClosedIcon>
|
||||
</div>
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
class="border-0 px-2"
|
||||
@click="triggerDownload('xlsx')"
|
||||
<SecondaryButton class="border-0 px-2" @click="triggerDownload('xlsx')"
|
||||
>Export as Excel</SecondaryButton
|
||||
>
|
||||
<SecondaryButton
|
||||
class="border-0 px-2"
|
||||
@click="triggerDownload('csv')"
|
||||
<SecondaryButton class="border-0 px-2" @click="triggerDownload('csv')"
|
||||
>Export as CSV</SecondaryButton
|
||||
>
|
||||
<SecondaryButton
|
||||
class="border-0 px-2"
|
||||
@click="triggerDownload('ods')"
|
||||
<SecondaryButton class="border-0 px-2" @click="triggerDownload('ods')"
|
||||
>Export as ODS
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<UpgradeModal v-model:show="showPremiumModal">
|
||||
<strong>PDF Reports</strong> are only available in solidtime
|
||||
Professional.
|
||||
<strong>PDF Reports</strong> are only available in solidtime Professional.
|
||||
</UpgradeModal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CheckCircleIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/20/solid';
|
||||
import { ArrowDownTrayIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/vue/20/solid';
|
||||
import { Modal, PrimaryButton } from '@/packages/ui/src';
|
||||
const props = defineProps<{
|
||||
exportUrl: string | null;
|
||||
@@ -19,30 +15,19 @@ function downloadCurrentExport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
closeable
|
||||
max-width="lg"
|
||||
:show="showExportModal"
|
||||
@close="showExportModal = false">
|
||||
<Modal closeable max-width="lg" :show="showExportModal" @close="showExportModal = false">
|
||||
<button
|
||||
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-ring transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
|
||||
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
|
||||
</button>
|
||||
<div class="text-center text-text-primary py-6">
|
||||
<div
|
||||
class="flex items-center font-semibold text-lg justify-center space-x-2 pb-2">
|
||||
<CheckCircleIcon
|
||||
class="text-text-tertiary w-6"></CheckCircleIcon>
|
||||
<div class="flex items-center font-semibold text-lg justify-center space-x-2 pb-2">
|
||||
<CheckCircleIcon class="text-text-tertiary w-6"></CheckCircleIcon>
|
||||
<span> Export Successful! </span>
|
||||
</div>
|
||||
<div class="text-center text-sm max-w-64 mx-auto">
|
||||
<p class="pb-5">
|
||||
Your export is ready, you can download it with the button
|
||||
below.
|
||||
</p>
|
||||
<PrimaryButton
|
||||
:icon="ArrowDownTrayIcon"
|
||||
@click="downloadCurrentExport"
|
||||
<p class="pb-5">Your export is ready, you can download it with the button below.</p>
|
||||
<PrimaryButton :icon="ArrowDownTrayIcon" @click="downloadCurrentExport"
|
||||
>Download</PrimaryButton
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -26,18 +26,8 @@ const iconClass = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="
|
||||
twMerge(
|
||||
activeClass
|
||||
)
|
||||
">
|
||||
<component
|
||||
:is="icon"
|
||||
:class="iconClass"
|
||||
></component>
|
||||
<Button variant="outline" size="sm" :class="twMerge(activeClass)">
|
||||
<component :is="icon" :class="iconClass"></component>
|
||||
<span class="text-nowrap"> {{ title }} </span>
|
||||
<div
|
||||
v-if="count"
|
||||
|
||||
@@ -8,12 +8,10 @@ const props = defineProps<{
|
||||
groupByOptions: { value: string; label: string; icon: Component }[];
|
||||
}>();
|
||||
const icon = computed(() => {
|
||||
return props.groupByOptions.find((option) => option.value === model.value)
|
||||
?.icon;
|
||||
return props.groupByOptions.find((option) => option.value === model.value)?.icon;
|
||||
});
|
||||
const title = computed(() => {
|
||||
return props.groupByOptions.find((option) => option.value === model.value)
|
||||
?.label;
|
||||
return props.groupByOptions.find((option) => option.value === model.value)?.label;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ChartBarIcon,
|
||||
CheckCircleIcon,
|
||||
TagIcon,
|
||||
UserGroupIcon,
|
||||
} from '@heroicons/vue/20/solid';
|
||||
import { ChartBarIcon, CheckCircleIcon, TagIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||
import { FolderIcon } from '@heroicons/vue/16/solid';
|
||||
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
@@ -43,11 +38,7 @@ import {
|
||||
type CreateReportBodyProperties,
|
||||
type Organization,
|
||||
} from '@/packages/api/src';
|
||||
import {
|
||||
getCurrentMembershipId,
|
||||
getCurrentOrganizationId,
|
||||
getCurrentRole,
|
||||
} from '@/utils/useUser';
|
||||
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
|
||||
import { useTagsStore } from '@/utils/useTags';
|
||||
import { useSessionStorage, useStorage } from '@vueuse/core';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
@@ -84,8 +75,7 @@ const subGroup = useStorage<GroupingOption>('reporting-sub-group', 'task');
|
||||
|
||||
const reportingStore = useReportingStore();
|
||||
|
||||
const { aggregatedGraphTimeEntries, aggregatedTableTimeEntries } =
|
||||
storeToRefs(reportingStore);
|
||||
const { aggregatedGraphTimeEntries, aggregatedTableTimeEntries } = storeToRefs(reportingStore);
|
||||
|
||||
const { groupByOptions } = reportingStore;
|
||||
|
||||
@@ -103,26 +93,13 @@ function getFilterAttributes(): AggregatedTimeEntriesQueryParams {
|
||||
};
|
||||
params = {
|
||||
...params,
|
||||
member_ids:
|
||||
selectedMembers.value.length > 0
|
||||
? selectedMembers.value
|
||||
: undefined,
|
||||
project_ids:
|
||||
selectedProjects.value.length > 0
|
||||
? selectedProjects.value
|
||||
: undefined,
|
||||
task_ids:
|
||||
selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
||||
client_ids:
|
||||
selectedClients.value.length > 0
|
||||
? selectedClients.value
|
||||
: undefined,
|
||||
member_ids: selectedMembers.value.length > 0 ? selectedMembers.value : undefined,
|
||||
project_ids: selectedProjects.value.length > 0 ? selectedProjects.value : undefined,
|
||||
task_ids: selectedTasks.value.length > 0 ? selectedTasks.value : undefined,
|
||||
client_ids: selectedClients.value.length > 0 ? selectedClients.value : undefined,
|
||||
tag_ids: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||
billable: billable.value !== null ? billable.value : undefined,
|
||||
member_id:
|
||||
getCurrentRole() === 'employee'
|
||||
? getCurrentMembershipId()
|
||||
: undefined,
|
||||
member_id: getCurrentRole() === 'employee' ? getCurrentMembershipId() : undefined,
|
||||
rounding_type: roundingEnabled.value ? roundingType.value : undefined,
|
||||
rounding_minutes: roundingEnabled.value ? roundingMinutes.value : undefined,
|
||||
};
|
||||
@@ -142,9 +119,7 @@ function updateGraphReporting() {
|
||||
function updateTableReporting() {
|
||||
const params = getFilterAttributes();
|
||||
if (group.value === subGroup.value) {
|
||||
const fallbackOption = groupByOptions.find(
|
||||
(el) => el.value !== group.value
|
||||
);
|
||||
const fallbackOption = groupByOptions.find((el) => el.value !== group.value);
|
||||
if (fallbackOption?.value) {
|
||||
subGroup.value = fallbackOption.value;
|
||||
}
|
||||
@@ -162,14 +137,8 @@ function updateReporting() {
|
||||
updateTableReporting();
|
||||
}
|
||||
|
||||
function getOptimalGroupingOption(
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): 'day' | 'week' | 'month' {
|
||||
const diffInDays = getDayJsInstance()(endDate).diff(
|
||||
getDayJsInstance()(startDate),
|
||||
'd'
|
||||
);
|
||||
function getOptimalGroupingOption(startDate: string, endDate: string): 'day' | 'week' | 'month' {
|
||||
const diffInDays = getDayJsInstance()(endDate).diff(getDayJsInstance()(startDate), 'd');
|
||||
|
||||
if (diffInDays <= 31) {
|
||||
return 'day';
|
||||
@@ -213,10 +182,7 @@ async function downloadExport(format: ExportFormat) {
|
||||
...getFilterAttributes(),
|
||||
group: group.value,
|
||||
sub_group: subGroup.value,
|
||||
history_group: getOptimalGroupingOption(
|
||||
startDate.value,
|
||||
endDate.value
|
||||
),
|
||||
history_group: getOptimalGroupingOption(startDate.value, endDate.value),
|
||||
format: format,
|
||||
},
|
||||
}),
|
||||
@@ -249,17 +215,12 @@ const groupedPieChartData = computed(() => {
|
||||
if (
|
||||
name &&
|
||||
aggregatedTableTimeEntries.value?.grouped_type &&
|
||||
emptyPlaceholder[
|
||||
aggregatedTableTimeEntries.value?.grouped_type
|
||||
] === name
|
||||
emptyPlaceholder[aggregatedTableTimeEntries.value?.grouped_type] === name
|
||||
) {
|
||||
color = '#CCCCCC';
|
||||
} else if (
|
||||
aggregatedTableTimeEntries.value?.grouped_type === 'project'
|
||||
) {
|
||||
} else if (aggregatedTableTimeEntries.value?.grouped_type === 'project') {
|
||||
color =
|
||||
projects.value?.find((project) => project.id === entry.key)
|
||||
?.color ?? '#CCCCCC';
|
||||
projects.value?.find((project) => project.id === entry.key)?.color ?? '#CCCCCC';
|
||||
}
|
||||
return {
|
||||
value: entry.seconds,
|
||||
@@ -288,10 +249,7 @@ const tableData = computed(() => {
|
||||
return {
|
||||
seconds: el.seconds,
|
||||
cost: el.cost,
|
||||
description: getNameForReportingRowEntry(
|
||||
el.key,
|
||||
entry.grouped_type
|
||||
),
|
||||
description: getNameForReportingRowEntry(el.key, entry.grouped_type),
|
||||
};
|
||||
}) ?? [],
|
||||
};
|
||||
@@ -310,20 +268,15 @@ const tableData = computed(() => {
|
||||
<ReportingTabNavbar active="reporting"></ReportingTabNavbar>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<ReportingExportButton
|
||||
:download="downloadExport"></ReportingExportButton>
|
||||
<ReportSaveButton
|
||||
:report-properties="reportProperties"></ReportSaveButton>
|
||||
<ReportingExportButton :download="downloadExport"></ReportingExportButton>
|
||||
<ReportSaveButton :report-properties="reportProperties"></ReportSaveButton>
|
||||
</div>
|
||||
</MainContainer>
|
||||
<div class="py-2.5 w-full border-b border-default-background-separator">
|
||||
<MainContainer class="sm:flex space-y-4 sm:space-y-0 justify-between">
|
||||
<div
|
||||
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-3">
|
||||
<div class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-3">
|
||||
<div class="text-sm font-medium">Filters</div>
|
||||
<MemberMultiselectDropdown
|
||||
v-model="selectedMembers"
|
||||
@submit="updateReporting">
|
||||
<MemberMultiselectDropdown v-model="selectedMembers" @submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedMembers.length"
|
||||
@@ -332,9 +285,7 @@ const tableData = computed(() => {
|
||||
:icon="UserGroupIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</MemberMultiselectDropdown>
|
||||
<ProjectMultiselectDropdown
|
||||
v-model="selectedProjects"
|
||||
@submit="updateReporting">
|
||||
<ProjectMultiselectDropdown v-model="selectedProjects" @submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedProjects.length"
|
||||
@@ -343,9 +294,7 @@ const tableData = computed(() => {
|
||||
:icon="FolderIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</ProjectMultiselectDropdown>
|
||||
<TaskMultiselectDropdown
|
||||
v-model="selectedTasks"
|
||||
@submit="updateReporting">
|
||||
<TaskMultiselectDropdown v-model="selectedTasks" @submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedTasks.length"
|
||||
@@ -354,9 +303,7 @@ const tableData = computed(() => {
|
||||
:icon="CheckCircleIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</TaskMultiselectDropdown>
|
||||
<ClientMultiselectDropdown
|
||||
v-model="selectedClients"
|
||||
@submit="updateReporting">
|
||||
<ClientMultiselectDropdown v-model="selectedClients" @submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedClients.length"
|
||||
@@ -401,11 +348,7 @@ const tableData = computed(() => {
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:active="billable !== null"
|
||||
:title="
|
||||
billable === 'false'
|
||||
? 'Non Billable'
|
||||
: 'Billable'
|
||||
"
|
||||
:title="billable === 'false' ? 'Non Billable' : 'Billable'"
|
||||
:icon="BillableIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</SelectDropdown>
|
||||
@@ -427,37 +370,26 @@ const tableData = computed(() => {
|
||||
<div class="pt-10 w-full px-3 relative">
|
||||
<ReportingChart
|
||||
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
|
||||
:grouped-data="
|
||||
aggregatedGraphTimeEntries?.grouped_data
|
||||
"></ReportingChart>
|
||||
:grouped-data="aggregatedGraphTimeEntries?.grouped_data"></ReportingChart>
|
||||
</div>
|
||||
</MainContainer>
|
||||
<MainContainer>
|
||||
<div class="sm:grid grid-cols-4 pt-6 items-start">
|
||||
<div
|
||||
class="col-span-3 bg-card-background rounded-lg border border-card-border pt-3">
|
||||
<div class="col-span-3 bg-card-background rounded-lg border border-card-border pt-3">
|
||||
<div
|
||||
class="text-sm flex text-text-primary items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
|
||||
<span>Group by</span>
|
||||
<ReportingGroupBySelect
|
||||
v-model="group"
|
||||
:group-by-options="groupByOptions"
|
||||
@changed="
|
||||
updateTableReporting
|
||||
"></ReportingGroupBySelect>
|
||||
@changed="updateTableReporting"></ReportingGroupBySelect>
|
||||
<span>and</span>
|
||||
<ReportingGroupBySelect
|
||||
v-model="subGroup"
|
||||
:group-by-options="
|
||||
groupByOptions.filter((el) => el.value !== group)
|
||||
"
|
||||
@changed="
|
||||
updateTableReporting
|
||||
"></ReportingGroupBySelect>
|
||||
:group-by-options="groupByOptions.filter((el) => el.value !== group)"
|
||||
@changed="updateTableReporting"></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 150px">
|
||||
<div
|
||||
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-tertiary [&>*]:pb-1.5 [&>*]:pt-1 text-text-secondary text-sm">
|
||||
<div class="pl-6">Name</div>
|
||||
@@ -475,13 +407,11 @@ const tableData = computed(() => {
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:type="aggregatedTableTimeEntries.grouped_type"
|
||||
:entry="entry"></ReportingRow>
|
||||
<div
|
||||
class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
||||
<div class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
||||
<div class="flex items-center pl-6 font-medium">
|
||||
<span>Total</span>
|
||||
</div>
|
||||
<div
|
||||
class="justify-end flex items-center font-medium">
|
||||
<div class="justify-end flex items-center font-medium">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
aggregatedTableTimeEntries.seconds,
|
||||
@@ -490,8 +420,7 @@ const tableData = computed(() => {
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
class="justify-end pr-6 flex items-center font-medium">
|
||||
<div class="justify-end pr-6 flex items-center font-medium">
|
||||
{{
|
||||
aggregatedTableTimeEntries.cost
|
||||
? formatCents(
|
||||
@@ -509,16 +438,13 @@ const tableData = computed(() => {
|
||||
<div
|
||||
v-else
|
||||
class="chart flex flex-col items-center justify-center py-12 col-span-3">
|
||||
<p class="text-lg text-text-primary font-medium">
|
||||
No time entries found
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 lg:px-4">
|
||||
<ReportingPieChart
|
||||
:data="groupedPieChartData"></ReportingPieChart>
|
||||
<ReportingPieChart :data="groupedPieChartData"></ReportingPieChart>
|
||||
</div>
|
||||
</div>
|
||||
</MainContainer>
|
||||
|
||||
@@ -14,14 +14,7 @@ import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
||||
import { useCssVariable } from '@/utils/useCssVariable';
|
||||
import type { Organization } from '@/packages/api/src';
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
PieChart,
|
||||
TitleComponent,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
]);
|
||||
use([CanvasRenderer, PieChart, TitleComponent, GridComponent, TooltipComponent, LegendComponent]);
|
||||
|
||||
provide(THEME_KEY, 'dark');
|
||||
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
import { Switch } from '@/Components/ui/switch';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/Components/ui/popover';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/Components/ui/select';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldInput,
|
||||
NumberFieldContent,
|
||||
NumberFieldIncrement,
|
||||
NumberFieldDecrement
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldInput,
|
||||
NumberFieldContent,
|
||||
NumberFieldIncrement,
|
||||
NumberFieldDecrement,
|
||||
} from '@/Components/ui/number-field';
|
||||
import { ArrowsUpDownIcon } from '@heroicons/vue/20/solid';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
@@ -30,7 +30,7 @@ const TimeEntryRoundingType = {
|
||||
Nearest: 'nearest' as const,
|
||||
} as const;
|
||||
|
||||
type TimeEntryRoundingType = typeof TimeEntryRoundingType[keyof typeof TimeEntryRoundingType];
|
||||
type TimeEntryRoundingType = (typeof TimeEntryRoundingType)[keyof typeof TimeEntryRoundingType];
|
||||
|
||||
interface Props {
|
||||
enabled: boolean;
|
||||
@@ -79,8 +79,8 @@ const selectedInterval = ref('');
|
||||
|
||||
// Compute the current interval value based on props
|
||||
const currentInterval = computed(() => {
|
||||
const predefined = predefinedIntervals.find(interval =>
|
||||
interval.value !== 'custom' && parseInt(interval.value) === props.minutes
|
||||
const predefined = predefinedIntervals.find(
|
||||
(interval) => interval.value !== 'custom' && parseInt(interval.value) === props.minutes
|
||||
);
|
||||
return predefined ? predefined.value : 'custom';
|
||||
});
|
||||
@@ -116,10 +116,14 @@ function handleCustomMinutesChange(value: string | number) {
|
||||
}
|
||||
|
||||
// Watch for changes in props.minutes
|
||||
watch(() => props.minutes, (newMinutes) => {
|
||||
customMinutes.value = newMinutes;
|
||||
initializeSelectedInterval();
|
||||
}, { immediate: true });
|
||||
watch(
|
||||
() => props.minutes,
|
||||
(newMinutes) => {
|
||||
customMinutes.value = newMinutes;
|
||||
initializeSelectedInterval();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(currentInterval, () => {
|
||||
initializeSelectedInterval();
|
||||
@@ -136,7 +140,9 @@ const activeClass = computed(() => {
|
||||
const iconClass = computed(() => {
|
||||
return twMerge(
|
||||
'w-4 h-4',
|
||||
props.enabled ? 'dark:text-accent-300/80 text-accent-400/80' : 'text-muted-foreground opacity-50'
|
||||
props.enabled
|
||||
? 'dark:text-accent-300/80 text-accent-400/80'
|
||||
: 'text-muted-foreground opacity-50'
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -144,10 +150,7 @@ const iconClass = computed(() => {
|
||||
<template>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="twMerge(activeClass)">
|
||||
<Button variant="outline" size="sm" :class="twMerge(activeClass)">
|
||||
<ArrowsUpDownIcon :class="iconClass" />
|
||||
Rounding {{ enabled ? 'on' : 'off' }}
|
||||
</Button>
|
||||
@@ -155,7 +158,9 @@ const iconClass = computed(() => {
|
||||
<PopoverContent class="w-72 p-4">
|
||||
<div v-if="!isAllowedToPerformPremiumAction()" class="flex flex-col space-y-2">
|
||||
<span class="font-semibold text-xs">Premium</span>
|
||||
<span class="text-xs text-text-secondary flex-1">Rounding is a premium feature. Upgrade to unlock this feature.</span>
|
||||
<span class="text-xs text-text-secondary flex-1"
|
||||
>Rounding is a premium feature. Upgrade to unlock this feature.</span
|
||||
>
|
||||
<Link href="/billing">
|
||||
<Button size="sm" variant="input" class="items-center space-x-1">
|
||||
<CreditCardIcon class="w-3.5 h-3.5 text-text-tertiary mr-1" />
|
||||
@@ -166,26 +171,31 @@ const iconClass = computed(() => {
|
||||
<div v-else class="space-y-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<InputLabel for="enable-rounding" value="Enable Rounding" />
|
||||
<Switch
|
||||
id="enable-rounding"
|
||||
:model-value="enabled"
|
||||
class="data-[state=checked]:bg-accent-500"
|
||||
@update:model-value="updateEnabled" />
|
||||
</div>
|
||||
<div class="mb-3 pb-2 pt-1 text-xs text-muted-foreground border-b border-border-secondary text-text-tertiary">
|
||||
Rounding is applied to each individual time entry, not to the accumulated total.
|
||||
</div>
|
||||
|
||||
<InputLabel for="enable-rounding" value="Enable Rounding" />
|
||||
<Switch
|
||||
id="enable-rounding"
|
||||
:model-value="enabled"
|
||||
class="data-[state=checked]:bg-accent-500"
|
||||
@update:model-value="updateEnabled" />
|
||||
</div>
|
||||
<div
|
||||
class="mb-3 pb-2 pt-1 text-xs text-muted-foreground border-b border-border-secondary text-text-tertiary">
|
||||
Rounding is applied to each individual time entry, not to the accumulated
|
||||
total.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="rounding-type" value="Rounding Type" class="mb-2" />
|
||||
<Select
|
||||
<Select
|
||||
:model-value="type"
|
||||
:disabled="!enabled"
|
||||
@update:model-value="(value) => updateType(value as TimeEntryRoundingType)">
|
||||
<SelectTrigger id="rounding-type" size="small" class="w-full" :disabled="!enabled">
|
||||
<SelectTrigger
|
||||
id="rounding-type"
|
||||
size="small"
|
||||
class="w-full"
|
||||
:disabled="!enabled">
|
||||
<SelectValue placeholder="Select rounding type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -197,23 +207,27 @@ const iconClass = computed(() => {
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="minutes-interval" value="Minutes Interval" class="mb-2" />
|
||||
<Select
|
||||
<Select
|
||||
:model-value="selectedInterval"
|
||||
:disabled="!enabled"
|
||||
@update:model-value="(value) => handleIntervalChange(value as string)">
|
||||
<SelectTrigger id="minutes-interval" size="small" class="w-full" :disabled="!enabled">
|
||||
<SelectValue placeholder="Select interval" />
|
||||
<SelectTrigger
|
||||
id="minutes-interval"
|
||||
size="small"
|
||||
class="w-full"
|
||||
:disabled="!enabled">
|
||||
<SelectValue placeholder="Select interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="interval in predefinedIntervals"
|
||||
<SelectItem
|
||||
v-for="interval in predefinedIntervals"
|
||||
:key="interval.value"
|
||||
:value="interval.value">
|
||||
{{ interval.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
||||
<div v-if="showCustomInput" class="mt-2">
|
||||
<NumberField
|
||||
id="custom-minutes"
|
||||
@@ -226,7 +240,9 @@ const iconClass = computed(() => {
|
||||
@update:model-value="handleCustomMinutesChange">
|
||||
<NumberFieldContent>
|
||||
<NumberFieldDecrement :disabled="!enabled" />
|
||||
<NumberFieldInput placeholder="Enter custom minutes" :disabled="!enabled" />
|
||||
<NumberFieldInput
|
||||
placeholder="Enter custom minutes"
|
||||
:disabled="!enabled" />
|
||||
<NumberFieldIncrement :disabled="!enabled" />
|
||||
</NumberFieldContent>
|
||||
</NumberField>
|
||||
@@ -235,4 +251,4 @@ const iconClass = computed(() => {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -32,10 +32,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
||||
class="contents text-text-primary [&>*]:transition [&>*]:border-card-background-separator [&>*]:border-b [&>*]:h-[50px]">
|
||||
<div
|
||||
:class="
|
||||
twMerge(
|
||||
'pl-6 font-medium flex items-center space-x-3',
|
||||
props.indent ? 'pl-16' : ''
|
||||
)
|
||||
twMerge('pl-6 font-medium flex items-center space-x-3', props.indent ? 'pl-16' : '')
|
||||
">
|
||||
<GroupedItemsCountButton
|
||||
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
|
||||
@@ -57,13 +54,17 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
||||
}}
|
||||
</div>
|
||||
<div class="justify-end pr-6 flex items-center">
|
||||
{{ entry.cost ? formatCents(
|
||||
entry.cost,
|
||||
props.currency,
|
||||
organization?.currency_format,
|
||||
organization?.currency_symbol,
|
||||
organization?.number_format
|
||||
) : '--' }}
|
||||
{{
|
||||
entry.cost
|
||||
? formatCents(
|
||||
entry.cost,
|
||||
props.currency,
|
||||
organization?.currency_format,
|
||||
organization?.currency_symbol,
|
||||
organization?.number_format
|
||||
)
|
||||
: '--'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
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";
|
||||
import { canViewReport } from '@/utils/permissions';
|
||||
import { computed } from 'vue';
|
||||
defineProps<{
|
||||
active: 'reporting' | 'detailed' | 'shared';
|
||||
}>();
|
||||
@@ -12,17 +12,11 @@ const showSharedReports = computed(() => canViewReport());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabBar
|
||||
:model-value="active"
|
||||
>
|
||||
<TabBarItem
|
||||
value="reporting"
|
||||
@click="router.visit(route('reporting'))"
|
||||
<TabBar :model-value="active">
|
||||
<TabBarItem value="reporting" @click="router.visit(route('reporting'))"
|
||||
>Overview</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
value="detailed"
|
||||
@click="router.visit(route('reporting.detailed'))"
|
||||
<TabBarItem value="detailed" @click="router.visit(route('reporting.detailed'))"
|
||||
>Detailed</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
|
||||
Reference in New Issue
Block a user