add support for interval / duration format in frontend views

This commit is contained in:
Gregor Vostrak
2025-05-07 19:39:23 +02:00
parent e374d8b3de
commit 8b950d99d6
24 changed files with 1219 additions and 843 deletions

View File

@@ -0,0 +1,41 @@
export type NumberFormat =
| 'point-comma'
| 'comma-point'
| 'space-comma'
| 'space-point'
| 'apostrophe-point';
/**
* Formats a number according to the specified format
* @param value - The number to format
* @param format - The format to use
* @returns The formatted number as a string
*/
export function formatNumber(value: number, format?: string): string {
// Convert to fixed 2 decimal places first
const parts = value.toFixed(2).split('.');
const wholePart = parts[0];
const decimalPart = parts[1];
// Format the whole number part based on the format
let formattedWhole: string;
switch (format) {
case 'point-comma':
formattedWhole = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return `${formattedWhole},${decimalPart}`;
case 'comma-point':
formattedWhole = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${formattedWhole}.${decimalPart}`;
case 'space-comma':
formattedWhole = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
return `${formattedWhole},${decimalPart}`;
case 'space-point':
formattedWhole = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
return `${formattedWhole}.${decimalPart}`;
case 'apostrophe-point':
formattedWhole = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, "'");
return `${formattedWhole}.${decimalPart}`;
default:
return value.toString();
}
}

View File

@@ -6,10 +6,34 @@ import isYesterday from 'dayjs/plugin/isYesterday';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import parse from 'parse-duration';
import { getUserTimezone, getWeekStart } from './settings';
import updateLocale from 'dayjs/plugin/updateLocale';
import { computed } from 'vue';
import { formatNumber } from './number';
export type CurrencyFormat =
| 'iso-code-before-with-space'
| 'iso-code-after-with-space'
| 'symbol-before'
| 'symbol-after'
| 'symbol-before-with-space'
| 'symbol-after-with-space';
export type DateFormat =
| 'point-separated-d-m-yyyy'
| 'slash-separated-mm-dd-yyyy'
| 'slash-separated-dd-mm-yyyy'
| 'hyphen-separated-dd-mm-yyyy'
| 'hyphen-separated-mm-dd-yyyy'
| 'hyphen-separated-yyyy-mm-dd';
export type TimeFormat = '12-hours' | '24-hours';
export type IntervalFormat =
| 'decimal'
| 'hours-minutes'
| 'hours-minutes-colon-separated'
| 'hours-minutes-seconds-colon-separated';
export type TimeInputUnit = 'minutes' | 'hours';
dayjs.extend(relativeTime);
dayjs.extend(isToday);
@@ -40,11 +64,28 @@ export const firstDayIndex = computed(() => {
return apiDayOrder.indexOf(getWeekStart());
});
export function formatHumanReadableDuration(duration: number): string {
export function formatHumanReadableDuration(
duration: number,
intervalFormat?: string,
numberFormat?: string
): string {
const dayJsDuration = dayjs.duration(duration, 's');
const hours = Math.floor(dayJsDuration.asHours());
const minutes = dayJsDuration.minutes();
return `${hours}h ${minutes.toString().padStart(2, '0')}min`;
const seconds = dayJsDuration.seconds();
switch (intervalFormat) {
case 'decimal':
return formatNumber(dayJsDuration.asHours(), numberFormat) + ' h';
case 'hours-minutes':
return `${hours}h ${minutes.toString().padStart(2, '0')}min`;
case 'hours-minutes-colon-separated':
return `${hours}:${minutes.toString().padStart(2, '0')}`;
case 'hours-minutes-seconds-colon-separated':
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
default:
return `${hours}h ${minutes.toString().padStart(2, '0')}min`;
}
}
export function formatDuration(duration: number): string {
@@ -131,3 +172,44 @@ export function formatStartEnd(start: string, end: string | null) {
return `${formatTime(start)} - ...`;
}
}
export function parseTimeInput(
input: string,
defaultUnit: TimeInputUnit = 'minutes'
): {
seconds: number | null;
isHHMM: boolean;
} {
// Check if input is a decimal number (hours)
const decimalRegex = /^-?\d+[.,]\d+$/;
if (decimalRegex.test(input)) {
const hours = parseFloat(input.replace(',', '.'));
return { seconds: Math.round(hours * 3600), isHHMM: false };
}
// Check if input is just a number (minutes or hours based on defaultUnit)
if (/^-?\d+$/.test(input)) {
const value = parseInt(input);
const seconds = defaultUnit === 'minutes' ? value * 60 : value * 3600;
return { seconds, isHHMM: false };
}
// Check if input is in HH:MM format
const HHMMtimeRegex = /^([0-9]{1,2}):([0-5]?[0-9])$/;
if (HHMMtimeRegex.test(input)) {
const match = input.match(HHMMtimeRegex);
if (match) {
const hours = parseInt(match[1]);
const minutes = parseInt(match[2]);
return { seconds: (hours * 60 + minutes) * 60, isHHMM: true };
}
}
// Try to parse natural language like "1h 30m"
const parsedDuration = parse(input, 's');
if (parsedDuration && parsedDuration > 0) {
return { seconds: parsedDuration, isHHMM: false };
}
return { seconds: null, isHHMM: false };
}