mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +01:00
promote duration formats that omit seconds to HH:mm:ss in reporting
views and exports
This commit is contained in:
@@ -96,6 +96,30 @@ class LocalizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a duration for reporting contexts (PDF reports, places that display duration
|
||||||
|
* directly next to cost). Promotes the verbose `Hh Mm` format to the compact `HH:MM:SS`
|
||||||
|
* so totals stay narrow and reconcile with cost, which is always computed to the second.
|
||||||
|
*/
|
||||||
|
public function formatIntervalForReporting(CarbonInterval $interval): string
|
||||||
|
{
|
||||||
|
$promoted = [
|
||||||
|
IntervalFormat::HoursMinutes,
|
||||||
|
IntervalFormat::HoursMinutesColonSeparated,
|
||||||
|
];
|
||||||
|
if (! in_array($this->intervalFormat, $promoted, true)) {
|
||||||
|
return $this->formatInterval($interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
$previous = $this->intervalFormat;
|
||||||
|
$this->intervalFormat = IntervalFormat::HoursMinutesSecondsColonSeparated;
|
||||||
|
try {
|
||||||
|
return $this->formatInterval($interval);
|
||||||
|
} finally {
|
||||||
|
$this->intervalFormat = $previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function formatCurrency(Money $money): string
|
public function formatCurrency(Money $money): string
|
||||||
{
|
{
|
||||||
$currencyService = app(CurrencyService::class);
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import VChart, { THEME_KEY } from 'vue-echarts';
|
import VChart, { THEME_KEY } from 'vue-echarts';
|
||||||
import { computed, provide, inject, shallowRef, type ComputedRef } from 'vue';
|
import { computed, provide, inject, shallowRef, type ComputedRef } from 'vue';
|
||||||
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
|
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 { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { BarChart } from 'echarts/charts';
|
import { BarChart } from 'echarts/charts';
|
||||||
@@ -137,7 +137,7 @@ const option = computed(() => ({
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueFormatter: (value: number) => {
|
valueFormatter: (value: number) => {
|
||||||
return formatHumanReadableDuration(
|
return formatReportingDuration(
|
||||||
value,
|
value,
|
||||||
organization?.value?.interval_format,
|
organization?.value?.interval_format,
|
||||||
organization?.value?.number_format
|
organization?.value?.number_format
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { SaveIcon } from 'lucide-vue-next';
|
import { SaveIcon } from 'lucide-vue-next';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import {
|
import {
|
||||||
formatHumanReadableDuration,
|
formatReportingDuration,
|
||||||
getDayJsInstance,
|
getDayJsInstance,
|
||||||
getLocalizedDayJs,
|
getLocalizedDayJs,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
@@ -426,7 +426,7 @@ const tableData = computed(() => {
|
|||||||
class="justify-end flex items-center font-medium"
|
class="justify-end flex items-center font-medium"
|
||||||
:class="!showBillableRate ? 'pr-6' : ''">
|
:class="!showBillableRate ? 'pr-6' : ''">
|
||||||
{{
|
{{
|
||||||
formatHumanReadableDuration(
|
formatReportingDuration(
|
||||||
aggregatedTableTimeEntries.seconds,
|
aggregatedTableTimeEntries.seconds,
|
||||||
organization?.interval_format,
|
organization?.interval_format,
|
||||||
organization?.number_format
|
organization?.number_format
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
} from 'echarts/components';
|
} 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 { useCssVariable } from '@/packages/ui/src';
|
||||||
import type { Organization } from '@/packages/api/src';
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ const option = computed(() => ({
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueFormatter: (value: number) => {
|
valueFormatter: (value: number) => {
|
||||||
return formatHumanReadableDuration(
|
return formatReportingDuration(
|
||||||
value,
|
value,
|
||||||
organization?.value?.interval_format,
|
organization?.value?.interval_format,
|
||||||
organization?.value?.number_format
|
organization?.value?.number_format
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
import GroupedItemsCountButton from '@/packages/ui/src/GroupedItemsCountButton.vue';
|
import GroupedItemsCountButton from '@/packages/ui/src/GroupedItemsCountButton.vue';
|
||||||
import { ref, inject, type ComputedRef } from 'vue';
|
import { ref, inject, type ComputedRef } from 'vue';
|
||||||
@@ -44,7 +44,7 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
|||||||
</div>
|
</div>
|
||||||
<div class="justify-end flex items-center" :class="!showCost ? 'pr-6' : ''">
|
<div class="justify-end flex items-center" :class="!showCost ? 'pr-6' : ''">
|
||||||
{{
|
{{
|
||||||
formatHumanReadableDuration(
|
formatReportingDuration(
|
||||||
entry.seconds,
|
entry.seconds,
|
||||||
organization?.interval_format,
|
organization?.interval_format,
|
||||||
organization?.number_format
|
organization?.number_format
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import CardTitle from '@/packages/ui/src/CardTitle.vue';
|
|||||||
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
|
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
|
||||||
import ProjectsChartCard from '@/Components/Dashboard/ProjectsChartCard.vue';
|
import ProjectsChartCard from '@/Components/Dashboard/ProjectsChartCard.vue';
|
||||||
import ThisWeekReportingTable from '@/Components/Dashboard/ThisWeekReportingTable.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 { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
import { getWeekStart } from '@/packages/ui/src/utils/settings';
|
import { getWeekStart } from '@/packages/ui/src/utils/settings';
|
||||||
import { useCssVariable } from '@/packages/ui/src';
|
import { useCssVariable } from '@/packages/ui/src';
|
||||||
@@ -223,7 +223,7 @@ const option = computed(() => {
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueFormatter: (value: number) => {
|
valueFormatter: (value: number) => {
|
||||||
return formatHumanReadableDuration(
|
return formatReportingDuration(
|
||||||
value,
|
value,
|
||||||
organization?.value?.interval_format,
|
organization?.value?.interval_format,
|
||||||
organization?.value?.number_format
|
organization?.value?.number_format
|
||||||
@@ -252,7 +252,7 @@ const option = computed(() => {
|
|||||||
title="Spent Time"
|
title="Spent Time"
|
||||||
:value="
|
:value="
|
||||||
totalWeeklyTime
|
totalWeeklyTime
|
||||||
? formatHumanReadableDuration(
|
? formatReportingDuration(
|
||||||
totalWeeklyTime,
|
totalWeeklyTime,
|
||||||
organization?.interval_format,
|
organization?.interval_format,
|
||||||
organization?.number_format
|
organization?.number_format
|
||||||
@@ -263,7 +263,7 @@ const option = computed(() => {
|
|||||||
title="Billable Time"
|
title="Billable Time"
|
||||||
:value="
|
:value="
|
||||||
totalWeeklyBillableTime
|
totalWeeklyBillableTime
|
||||||
? formatHumanReadableDuration(
|
? formatReportingDuration(
|
||||||
totalWeeklyBillableTime,
|
totalWeeklyBillableTime,
|
||||||
organization?.interval_format,
|
organization?.interval_format,
|
||||||
organization?.number_format
|
organization?.number_format
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||||
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
|
import ReportingGroupBySelect from '@/Components/Common/Reporting/ReportingGroupBySelect.vue';
|
||||||
import {
|
import {
|
||||||
formatHumanReadableDuration,
|
formatReportingDuration,
|
||||||
getDayJsInstance,
|
getDayJsInstance,
|
||||||
getLocalizedDayJs,
|
getLocalizedDayJs,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
@@ -174,7 +174,7 @@ const showBillableRate = computed(() => {
|
|||||||
class="justify-end flex items-center font-medium"
|
class="justify-end flex items-center font-medium"
|
||||||
:class="!showBillableRate ? 'pr-6' : ''">
|
:class="!showBillableRate ? 'pr-6' : ''">
|
||||||
{{
|
{{
|
||||||
formatHumanReadableDuration(
|
formatReportingDuration(
|
||||||
aggregatedTableTimeEntries.seconds,
|
aggregatedTableTimeEntries.seconds,
|
||||||
organization?.interval_format,
|
organization?.interval_format,
|
||||||
organization?.number_format
|
organization?.number_format
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
:organization-billable-rate="organization?.billable_rate ?? null"
|
:organization-billable-rate="organization?.billable_rate ?? null"
|
||||||
:duplicate-time-entry="() => createTimeEntry(entry)"
|
:duplicate-time-entry="() => createTimeEntry(entry)"
|
||||||
:members="members"
|
:members="members"
|
||||||
|
is-report
|
||||||
show-date
|
show-date
|
||||||
show-member
|
show-member
|
||||||
:time-entry="entry"
|
:time-entry="entry"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
|||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import { ChartBarIcon } from '@heroicons/vue/20/solid';
|
import { ChartBarIcon } from '@heroicons/vue/20/solid';
|
||||||
import ReportingChart from '@/Components/Common/Reporting/ReportingChart.vue';
|
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 ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||||
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
@@ -231,7 +231,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="justify-end flex items-center font-medium">
|
<div class="justify-end flex items-center font-medium">
|
||||||
{{
|
{{
|
||||||
formatHumanReadableDuration(
|
formatReportingDuration(
|
||||||
aggregatedTableTimeEntries.seconds,
|
aggregatedTableTimeEntries.seconds,
|
||||||
reportIntervalFormat,
|
reportIntervalFormat,
|
||||||
reportNumberFormat
|
reportNumberFormat
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import FormSection from '@/Components/FormSection.vue';
|
import FormSection from '@/Components/FormSection.vue';
|
||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { Field, FieldLabel } from '@/packages/ui/src/field';
|
import { Field, FieldDescription, FieldLabel } from '@/packages/ui/src/field';
|
||||||
import type { UpdateOrganizationBody } from '@/packages/api/src';
|
import type { UpdateOrganizationBody } from '@/packages/api/src';
|
||||||
import { useOrganizationStore } from '@/utils/useOrganization';
|
import { useOrganizationStore } from '@/utils/useOrganization';
|
||||||
import { storeToRefs } from 'pinia';
|
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() {
|
async function submit() {
|
||||||
mutation.mutate(form.value);
|
mutation.mutate(form.value);
|
||||||
}
|
}
|
||||||
@@ -149,6 +155,12 @@ async function submit() {
|
|||||||
>
|
>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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>
|
</Field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const props = defineProps<{
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
canCreateProject: boolean;
|
canCreateProject: boolean;
|
||||||
enableEstimatedTime: boolean;
|
enableEstimatedTime: boolean;
|
||||||
|
isReport?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{ selected: []; unselected: [] }>();
|
const emit = defineEmits<{ selected: []; unselected: [] }>();
|
||||||
@@ -172,6 +173,7 @@ async function handleDeleteTimeEntry() {
|
|||||||
<TimeEntryRowDurationInput
|
<TimeEntryRowDurationInput
|
||||||
:start="timeEntry.start"
|
:start="timeEntry.start"
|
||||||
:end="timeEntry.end"
|
:end="timeEntry.end"
|
||||||
|
:is-report="props.isReport"
|
||||||
@changed="updateStartEndTime"></TimeEntryRowDurationInput>
|
@changed="updateStartEndTime"></TimeEntryRowDurationInput>
|
||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
:active="!!(timeEntry.start && !timeEntry.end)"
|
:active="!!(timeEntry.start && !timeEntry.end)"
|
||||||
@@ -196,6 +198,7 @@ async function handleDeleteTimeEntry() {
|
|||||||
<TimeEntryRowDurationInput
|
<TimeEntryRowDurationInput
|
||||||
:start="timeEntry.start"
|
:start="timeEntry.start"
|
||||||
:end="timeEntry.end"
|
:end="timeEntry.end"
|
||||||
|
:is-report="props.isReport"
|
||||||
@changed="updateStartEndTime"></TimeEntryRowDurationInput>
|
@changed="updateStartEndTime"></TimeEntryRowDurationInput>
|
||||||
</div>
|
</div>
|
||||||
<!-- Second row: project/task - tags - billable - start - more -->
|
<!-- Second row: project/task - tags - billable - start - more -->
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
calculateDifference,
|
calculateDifference,
|
||||||
formatHumanReadableDuration,
|
formatHumanReadableDuration,
|
||||||
|
formatReportingDuration,
|
||||||
parseTimeInput,
|
parseTimeInput,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
import { computed, ref, inject, type ComputedRef } from 'vue';
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||||
@@ -18,6 +19,7 @@ const organizationSettings = computed(() => ({
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
start: string;
|
start: string;
|
||||||
end: string | null;
|
end: string | null;
|
||||||
|
isReport?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
changed: [start: string, end: string | null];
|
changed: [start: string, end: string | null];
|
||||||
@@ -51,7 +53,8 @@ const currentTime = computed({
|
|||||||
if (temporaryCustomTimerEntry.value !== '') {
|
if (temporaryCustomTimerEntry.value !== '') {
|
||||||
return temporaryCustomTimerEntry.value;
|
return temporaryCustomTimerEntry.value;
|
||||||
}
|
}
|
||||||
return formatHumanReadableDuration(
|
const formatter = props.isReport ? formatReportingDuration : formatHumanReadableDuration;
|
||||||
|
return formatter(
|
||||||
calculateDifference(props.start, props.end),
|
calculateDifference(props.start, props.end),
|
||||||
organizationSettings.value.intervalFormat,
|
organizationSettings.value.intervalFormat,
|
||||||
organizationSettings.value.numberFormat
|
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 {
|
export function formatDuration(duration: number): string {
|
||||||
const dayJsDuration = dayjs.duration(duration, 's');
|
const dayJsDuration = dayjs.duration(duration, 's');
|
||||||
const hours = Math.floor(dayJsDuration.asHours());
|
const hours = Math.floor(dayJsDuration.asHours());
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||||
<div style="color: #71717a; font-weight: 600;">Duration</div>
|
<div style="color: #71717a; font-weight: 600;">Duration</div>
|
||||||
<div
|
<div
|
||||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatIntervalForReporting(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
||||||
</div>
|
</div>
|
||||||
@if($showBillableRate)
|
@if($showBillableRate)
|
||||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: left;">
|
<td style="text-align: left;">
|
||||||
{{ $localization->formatInterval(CarbonInterval::seconds($group1Entry['seconds'])) }}
|
{{ $localization->formatIntervalForReporting(CarbonInterval::seconds($group1Entry['seconds'])) }}
|
||||||
</td>
|
</td>
|
||||||
@if($showBillableRate)
|
@if($showBillableRate)
|
||||||
<td style="text-align: right;">
|
<td style="text-align: right;">
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
Total
|
Total
|
||||||
</td>
|
</td>
|
||||||
<td style="font-weight: 500;color: #18181b;">
|
<td style="font-weight: 500;color: #18181b;">
|
||||||
{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }}
|
{{ $localization->formatIntervalForReporting(CarbonInterval::seconds($aggregatedData['seconds'])) }}
|
||||||
</td>
|
</td>
|
||||||
@if($showBillableRate)
|
@if($showBillableRate)
|
||||||
<td style="text-align: right; font-weight: 500;color: #18181b;">
|
<td style="text-align: right; font-weight: 500;color: #18181b;">
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ $localization->formatInterval($duration) }}
|
{{ $localization->formatIntervalForReporting($duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ $localization->formatNumber($duration->totalHours) }}
|
{{ $localization->formatNumber($duration->totalHours) }}
|
||||||
@@ -403,7 +403,7 @@
|
|||||||
type: "bar",
|
type: "bar",
|
||||||
data: {!! json_encode(collect($dataHistoryChart['grouped_data'])->map(fn($value) => (object) [
|
data: {!! json_encode(collect($dataHistoryChart['grouped_data'])->map(fn($value) => (object) [
|
||||||
'value' => $value['seconds'],
|
'value' => $value['seconds'],
|
||||||
'name' => ((int) $value['seconds']) === 0 ? '' : $localization->formatInterval(CarbonInterval::seconds((int) $value['seconds']))
|
'name' => ((int) $value['seconds']) === 0 ? '' : $localization->formatIntervalForReporting(CarbonInterval::seconds((int) $value['seconds']))
|
||||||
])->toArray()) !!},
|
])->toArray()) !!},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: "#7dd3fc",
|
borderColor: "#7dd3fc",
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||||
<div style="color: #71717a; font-weight: 600;">Duration</div>
|
<div style="color: #71717a; font-weight: 600;">Duration</div>
|
||||||
<div
|
<div
|
||||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatIntervalForReporting(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
||||||
</div>
|
</div>
|
||||||
@if($showBillableRate)
|
@if($showBillableRate)
|
||||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
{{ $localization->formatTime($timeEntry->start->timezone($timezone)) }} - {{ $localization->formatTime($timeEntry->end->timezone($timezone)) }}
|
{{ $localization->formatTime($timeEntry->start->timezone($timezone)) }} - {{ $localization->formatTime($timeEntry->end->timezone($timezone)) }}
|
||||||
</td>
|
</td>
|
||||||
<td style="overflow-wrap: break-word; min-width: 75px;">
|
<td style="overflow-wrap: break-word; min-width: 75px;">
|
||||||
{{ $localization->formatInterval($timeEntry->getDuration()) }}
|
{{ $localization->formatIntervalForReporting($timeEntry->getDuration()) }}
|
||||||
</td>
|
</td>
|
||||||
<td style="overflow-wrap: break-word;">{{ $timeEntry->billable ? 'Yes' : 'No' }}</td>
|
<td style="overflow-wrap: break-word;">{{ $timeEntry->billable ? 'Yes' : 'No' }}</td>
|
||||||
<td style="overflow-wrap: break-word; min-width: 75px;">{{ count($timeEntry->tagsRelation) === 0 ? '-' : $timeEntry->tagsRelation->implode('name', ', ') }}</td>
|
<td style="overflow-wrap: break-word; min-width: 75px;">{{ count($timeEntry->tagsRelation) === 0 ? '-' : $timeEntry->tagsRelation->implode('name', ', ') }}</td>
|
||||||
|
|||||||
@@ -129,6 +129,58 @@ class LocalizationServiceTest extends TestCaseWithDatabase
|
|||||||
$this->assertSame('30001:03:04', $formatted);
|
$this->assertSame('30001:03:04', $formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_format_interval_for_reporting_with_type_decimal(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||||
|
$this->localizationService->setIntervalFormat(IntervalFormat::Decimal);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$formatted = $this->localizationService->formatIntervalForReporting($interval);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame('30.001,05 h', $formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_format_interval_for_reporting_with_type_hours_minutes(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||||
|
$this->localizationService->setIntervalFormat(IntervalFormat::HoursMinutes);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$formatted = $this->localizationService->formatIntervalForReporting($interval);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame('30001:03:04', $formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_format_interval_for_reporting_with_type_hours_minutes_colon_separated(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||||
|
$this->localizationService->setIntervalFormat(IntervalFormat::HoursMinutesColonSeparated);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$formatted = $this->localizationService->formatIntervalForReporting($interval);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame('30001:03:04', $formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_format_interval_for_reporting_with_type_hours_minutes_seconds_colon_separated(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||||
|
$this->localizationService->setIntervalFormat(IntervalFormat::HoursMinutesSecondsColonSeparated);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$formatted = $this->localizationService->formatIntervalForReporting($interval);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertSame('30001:03:04', $formatted);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_format_currency_with_type_symbol_after_with_space_and_number_format_thousands_space_decimal_comma(): void
|
public function test_format_currency_with_type_symbol_after_with_space_and_number_format_thousands_space_decimal_comma(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
Reference in New Issue
Block a user