mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
promote duration formats that omit seconds to HH:mm:ss in reporting
views and exports
This commit is contained in:
@@ -2,7 +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, formatReportingDuration, formatWeek } from '@/packages/ui/src/utils/time';
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { BarChart } from 'echarts/charts';
|
||||
@@ -137,7 +137,7 @@ const option = computed(() => ({
|
||||
type: 'bar',
|
||||
tooltip: {
|
||||
valueFormatter: (value: number) => {
|
||||
return formatHumanReadableDuration(
|
||||
return formatReportingDuration(
|
||||
value,
|
||||
organization?.value?.interval_format,
|
||||
organization?.value?.number_format
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { SaveIcon } from 'lucide-vue-next';
|
||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
import {
|
||||
formatHumanReadableDuration,
|
||||
formatReportingDuration,
|
||||
getDayJsInstance,
|
||||
getLocalizedDayJs,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
@@ -426,7 +426,7 @@ const tableData = computed(() => {
|
||||
class="justify-end flex items-center font-medium"
|
||||
:class="!showBillableRate ? 'pr-6' : ''">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
formatReportingDuration(
|
||||
aggregatedTableTimeEntries.seconds,
|
||||
organization?.interval_format,
|
||||
organization?.number_format
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatReportingDuration } from '@/packages/ui/src/utils/time';
|
||||
import { useCssVariable } from '@/packages/ui/src';
|
||||
import type { Organization } from '@/packages/api/src';
|
||||
|
||||
@@ -67,7 +67,7 @@ const option = computed(() => ({
|
||||
},
|
||||
tooltip: {
|
||||
valueFormatter: (value: number) => {
|
||||
return formatHumanReadableDuration(
|
||||
return formatReportingDuration(
|
||||
value,
|
||||
organization?.value?.interval_format,
|
||||
organization?.value?.number_format
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatReportingDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||
import GroupedItemsCountButton from '@/packages/ui/src/GroupedItemsCountButton.vue';
|
||||
import { ref, inject, type ComputedRef } from 'vue';
|
||||
@@ -44,7 +44,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
||||
</div>
|
||||
<div class="justify-end flex items-center" :class="!showCost ? 'pr-6' : ''">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
formatReportingDuration(
|
||||
entry.seconds,
|
||||
organization?.interval_format,
|
||||
organization?.number_format
|
||||
|
||||
@@ -16,7 +16,7 @@ import CardTitle from '@/packages/ui/src/CardTitle.vue';
|
||||
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
|
||||
import ProjectsChartCard from '@/Components/Dashboard/ProjectsChartCard.vue';
|
||||
import ThisWeekReportingTable from '@/Components/Dashboard/ThisWeekReportingTable.vue';
|
||||
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatReportingDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||
import { getWeekStart } from '@/packages/ui/src/utils/settings';
|
||||
import { useCssVariable } from '@/packages/ui/src';
|
||||
@@ -223,7 +223,7 @@ const option = computed(() => {
|
||||
type: 'bar',
|
||||
tooltip: {
|
||||
valueFormatter: (value: number) => {
|
||||
return formatHumanReadableDuration(
|
||||
return formatReportingDuration(
|
||||
value,
|
||||
organization?.value?.interval_format,
|
||||
organization?.value?.number_format
|
||||
@@ -252,7 +252,7 @@ const option = computed(() => {
|
||||
title="Spent Time"
|
||||
:value="
|
||||
totalWeeklyTime
|
||||
? formatHumanReadableDuration(
|
||||
? formatReportingDuration(
|
||||
totalWeeklyTime,
|
||||
organization?.interval_format,
|
||||
organization?.number_format
|
||||
@@ -263,7 +263,7 @@ const option = computed(() => {
|
||||
title="Billable Time"
|
||||
:value="
|
||||
totalWeeklyBillableTime
|
||||
? formatHumanReadableDuration(
|
||||
? formatReportingDuration(
|
||||
totalWeeklyBillableTime,
|
||||
organization?.interval_format,
|
||||
organization?.number_format
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
|
||||
import {
|
||||
formatHumanReadableDuration,
|
||||
formatReportingDuration,
|
||||
getDayJsInstance,
|
||||
getLocalizedDayJs,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
@@ -174,7 +174,7 @@ const showBillableRate = computed(() => {
|
||||
class="justify-end flex items-center font-medium"
|
||||
:class="!showBillableRate ? 'pr-6' : ''">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
formatReportingDuration(
|
||||
aggregatedTableTimeEntries.seconds,
|
||||
organization?.interval_format,
|
||||
organization?.number_format
|
||||
|
||||
@@ -390,6 +390,7 @@ async function downloadExport(format: ExportFormat) {
|
||||
:organization-billable-rate="organization?.billable_rate ?? null"
|
||||
:duplicate-time-entry="() => createTimeEntry(entry)"
|
||||
:members="members"
|
||||
is-report
|
||||
show-date
|
||||
show-member
|
||||
:time-entry="entry"
|
||||
|
||||
@@ -3,7 +3,7 @@ import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||
import { ChartBarIcon } from '@heroicons/vue/20/solid';
|
||||
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
|
||||
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
||||
import { formatReportingDuration } from '@/packages/ui/src/utils/time';
|
||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||
@@ -231,7 +231,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="justify-end flex items-center font-medium">
|
||||
{{
|
||||
formatHumanReadableDuration(
|
||||
formatReportingDuration(
|
||||
aggregatedTableTimeEntries.seconds,
|
||||
reportIntervalFormat,
|
||||
reportNumberFormat
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { Field, FieldLabel } from '@/packages/ui/src/field';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Field, FieldDescription, FieldLabel } from '@/packages/ui/src/field';
|
||||
import type { UpdateOrganizationBody } from '@/packages/api/src';
|
||||
import { useOrganizationStore } from '@/utils/useOrganization';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -52,6 +52,12 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const showsHhMmSsInReports = computed(
|
||||
() =>
|
||||
form.value.interval_format === 'hours-minutes' ||
|
||||
form.value.interval_format === 'hours-minutes-colon-separated'
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
mutation.mutate(form.value);
|
||||
}
|
||||
@@ -149,6 +155,12 @@ async function submit() {
|
||||
>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription v-if="showsHhMmSsInReports">
|
||||
Reports and totals shown next to cost use HH:MM:SS for this format, so the
|
||||
duration reconciles with the billable amount down to the second. Everywhere
|
||||
else (time tracker, calendar, entry rows) seconds are omitted and durations
|
||||
stay in your chosen format.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ const props = defineProps<{
|
||||
selected?: boolean;
|
||||
canCreateProject: boolean;
|
||||
enableEstimatedTime: boolean;
|
||||
isReport?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ selected: []; unselected: [] }>();
|
||||
@@ -172,6 +173,7 @@ async function handleDeleteTimeEntry() {
|
||||
<TimeEntryRowDurationInput
|
||||
:start="timeEntry.start"
|
||||
:end="timeEntry.end"
|
||||
:is-report="props.isReport"
|
||||
@changed="updateStartEndTime"></TimeEntryRowDurationInput>
|
||||
<TimeTrackerStartStop
|
||||
:active="!!(timeEntry.start && !timeEntry.end)"
|
||||
@@ -196,6 +198,7 @@ async function handleDeleteTimeEntry() {
|
||||
<TimeEntryRowDurationInput
|
||||
:start="timeEntry.start"
|
||||
:end="timeEntry.end"
|
||||
:is-report="props.isReport"
|
||||
@changed="updateStartEndTime"></TimeEntryRowDurationInput>
|
||||
</div>
|
||||
<!-- Second row: project/task - tags - billable - start - more -->
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
calculateDifference,
|
||||
formatHumanReadableDuration,
|
||||
formatReportingDuration,
|
||||
parseTimeInput,
|
||||
} from '@/packages/ui/src/utils/time';
|
||||
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||
@@ -18,6 +19,7 @@ const organizationSettings = computed(() => ({
|
||||
const props = defineProps<{
|
||||
start: string;
|
||||
end: string | null;
|
||||
isReport?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
changed: [start: string, end: string | null];
|
||||
@@ -51,7 +53,8 @@ const currentTime = computed({
|
||||
if (temporaryCustomTimerEntry.value !== '') {
|
||||
return temporaryCustomTimerEntry.value;
|
||||
}
|
||||
return formatHumanReadableDuration(
|
||||
const formatter = props.isReport ? formatReportingDuration : formatHumanReadableDuration;
|
||||
return formatter(
|
||||
calculateDifference(props.start, props.end),
|
||||
organizationSettings.value.intervalFormat,
|
||||
organizationSettings.value.numberFormat
|
||||
|
||||
@@ -118,6 +118,26 @@ export function formatHumanReadableDuration(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a duration for reporting views where cost and duration must reconcile.
|
||||
*
|
||||
* When the org's `hours-minutes` format is selected, seconds are normally dropped for
|
||||
* readability (e.g. "14h 45min"). In reports this can make the total duration appear
|
||||
* inconsistent with the billable cost (which is computed to the second). To keep the
|
||||
* two columns reconcilable without inflating column widths with "14h 45min 06s",
|
||||
* promote to the compact `HH:MM:SS` format in reporting contexts.
|
||||
*/
|
||||
export function formatReportingDuration(
|
||||
duration: number,
|
||||
intervalFormat?: string,
|
||||
numberFormat?: string
|
||||
): string {
|
||||
const promoted =
|
||||
intervalFormat === 'hours-minutes' || intervalFormat === 'hours-minutes-colon-separated';
|
||||
const effectiveFormat = promoted ? 'hours-minutes-seconds-colon-separated' : intervalFormat;
|
||||
return formatHumanReadableDuration(duration, effectiveFormat, numberFormat);
|
||||
}
|
||||
|
||||
export function formatDuration(duration: number): string {
|
||||
const dayJsDuration = dayjs.duration(duration, 's');
|
||||
const hours = Math.floor(dayJsDuration.asHours());
|
||||
|
||||
Reference in New Issue
Block a user