diff --git a/e2e/reporting.spec.ts b/e2e/reporting.spec.ts
index 50b773da..1c855dbd 100644
--- a/e2e/reporting.spec.ts
+++ b/e2e/reporting.spec.ts
@@ -841,6 +841,50 @@ test('test that setting group by to current sub group triggers sub group fallbac
await expect(groupBySelects.filter({ hasText: 'Members' }).first()).toBeVisible();
});
+test('test that group by date groups the report by day and formats the date labels with organization settings', async ({
+ page,
+ ctx,
+}) => {
+ await updateOrganizationSettingViaApi(ctx, { date_format: 'point-separated-d-m-yyyy' });
+
+ await createTimeEntryViaApi(ctx, {
+ description: 'Entry for group by date',
+ duration: '1h',
+ });
+
+ // Go to reporting page
+ await goToReporting(page);
+ await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
+
+ // Find the "Group by" selects within the reporting table
+ const groupBySelects = page.locator('[data-testid="reporting_view"]').getByRole('combobox');
+
+ // Default state: group=Project
+ await groupBySelects.filter({ hasText: 'Project' }).first().click();
+
+ const [aggregateResponse] = await Promise.all([
+ page.waitForResponse(
+ (response) =>
+ response.url().includes('/time-entries/aggregate') &&
+ response.url().includes('group=day') &&
+ response.status() === 200
+ ),
+ page.getByRole('option', { name: 'Date', exact: true }).click(),
+ ]);
+
+ // Verify the API request contains the correct group parameter
+ const requestUrl = new URL(aggregateResponse.url());
+ expect(requestUrl.searchParams.get('group')).toBe('day');
+
+ // The row label is rendered in the organization date format (D.M.YYYY)
+ await expect(
+ page.getByTestId('reporting_view').getByText(/^\d{1,2}\.\d{1,2}\.\d{4}$/)
+ ).toBeVisible();
+ await expect(page.getByTestId('reporting_view').getByText(/^\d{4}-\d{2}-\d{2}$/)).toHaveCount(
+ 0
+ );
+});
+
// ──────────────────────────────────────────────────
// Export Tests
// ──────────────────────────────────────────────────
diff --git a/e2e/shared-reports.spec.ts b/e2e/shared-reports.spec.ts
index ff262b17..52541705 100644
--- a/e2e/shared-reports.spec.ts
+++ b/e2e/shared-reports.spec.ts
@@ -16,6 +16,7 @@ import {
createTimeEntryWithBillableStatusViaApi,
createTagViaApi,
createReportViaApi,
+ updateOrganizationSettingViaApi,
} from './utils/api';
import {
goToReporting,
@@ -68,6 +69,42 @@ test('test that saving a report creates a shared report and its shareable link s
await expect(page.getByText('Total')).toBeVisible();
});
+test('test that a shared report grouped by date shows date labels formatted by the organization setting', async ({
+ page,
+ ctx,
+}) => {
+ const reportName = 'DateGroupReport ' + Math.floor(Math.random() * 10000);
+
+ await updateOrganizationSettingViaApi(ctx, { date_format: 'point-separated-d-m-yyyy' });
+ await createTimeEntryViaApi(ctx, {
+ description: 'Entry for date grouping',
+ duration: '1h',
+ });
+
+ await goToReporting(page);
+
+ // Switch the grouping to "Date"
+ const groupBySelects = page.locator('[data-testid="reporting_view"]').getByRole('combobox');
+ await groupBySelects.filter({ hasText: 'Project' }).first().click();
+ await Promise.all([
+ page.waitForResponse(
+ (response) =>
+ response.url().includes('/time-entries/aggregate') &&
+ response.url().includes('group=day') &&
+ response.status() === 200
+ ),
+ page.getByRole('option', { name: 'Date', exact: true }).click(),
+ ]);
+
+ const { shareableLink } = await saveAsSharedReport(page, reportName);
+
+ // Verify row labels are formatted correctly
+ await page.goto(shareableLink);
+ await expect(page.getByText('Total')).toBeVisible();
+ await expect(page.getByText(/^\d{1,2}\.\d{1,2}\.\d{4}$/)).toBeVisible();
+ await expect(page.getByText(/^\d{4}-\d{2}-\d{2}$/)).toHaveCount(0);
+});
+
test('test that shared report with invalid secret shows no data', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/shared-report#invalid-secret-value');
await expect(page.getByText('No time entries found').first()).toBeVisible();
diff --git a/resources/js/Components/Common/Reporting/ReportingOverview.vue b/resources/js/Components/Common/Reporting/ReportingOverview.vue
index 04fcefa9..444dd716 100644
--- a/resources/js/Components/Common/Reporting/ReportingOverview.vue
+++ b/resources/js/Components/Common/Reporting/ReportingOverview.vue
@@ -240,7 +240,8 @@ const groupedPieChartData = computed(() => {
aggregatedTableTimeEntries.value?.grouped_data?.map((entry) => {
const name = getNameForReportingRowEntry(
entry.key,
- aggregatedTableTimeEntries.value?.grouped_type ?? null
+ aggregatedTableTimeEntries.value?.grouped_type ?? null,
+ organization?.value?.date_format
);
let color = getRandomColorWithSeed(entry.key ?? 'none');
if (
@@ -255,11 +256,7 @@ const groupedPieChartData = computed(() => {
}
return {
value: entry.seconds,
- name:
- getNameForReportingRowEntry(
- entry.key,
- aggregatedTableTimeEntries.value?.grouped_type ?? null
- ) ?? '',
+ name: name ?? '',
color: color,
};
}) ?? []
@@ -269,18 +266,25 @@ const groupedPieChartData = computed(() => {
const tableData = computed(() => {
return aggregatedTableTimeEntries.value?.grouped_data?.map((entry) => {
return {
+ key: entry.key,
seconds: entry.seconds,
cost: entry.cost,
description: getNameForReportingRowEntry(
entry.key,
- aggregatedTableTimeEntries.value?.grouped_type ?? null
+ aggregatedTableTimeEntries.value?.grouped_type ?? null,
+ organization?.value?.date_format
),
grouped_data:
entry.grouped_data?.map((el) => {
return {
+ key: el.key,
seconds: el.seconds,
cost: el.cost,
- description: getNameForReportingRowEntry(el.key, entry.grouped_type),
+ description: getNameForReportingRowEntry(
+ el.key,
+ entry.grouped_type,
+ organization?.value?.date_format
+ ),
};
}) ?? [],
};
@@ -421,9 +425,8 @@ const tableData = computed(() => {
">