diff --git a/e2e/shared-reports.spec.ts b/e2e/shared-reports.spec.ts
index b6233442..dfffd1a1 100644
--- a/e2e/shared-reports.spec.ts
+++ b/e2e/shared-reports.spec.ts
@@ -1,6 +1,10 @@
import { expect } from '@playwright/test';
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc.js';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import { test } from '../playwright/fixtures';
+
+dayjs.extend(utc);
import {
createProjectViaApi,
createClientViaApi,
@@ -11,6 +15,7 @@ import {
createBillableProjectViaApi,
createTimeEntryWithBillableStatusViaApi,
createTagViaApi,
+ createReportViaApi,
} from './utils/api';
import {
goToReporting,
@@ -766,6 +771,97 @@ test('test that updating expiration date on already-public report works', async
expect(returnedDate.getTime()).toBeGreaterThan(now.getTime());
});
+test('test that clearing the expiration date on a report works', async ({ page, ctx }) => {
+ const reportName = 'ClearExpReport ' + Math.floor(Math.random() * 10000);
+
+ // Create a public report with an expiration date via API
+ await createReportViaApi(ctx, {
+ name: reportName,
+ is_public: true,
+ public_until: dayjs().add(1, 'month').utc().format('YYYY-MM-DDTHH:mm:ss[Z]'),
+ });
+
+ // Go to shared reports and edit the report
+ await goToReportingShared(page);
+ await expect(page.getByText(reportName)).toBeVisible();
+
+ await page
+ .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
+ .click();
+ await page.getByRole('menuitem', { name: /^Edit Report/ }).click();
+ await expect(page.getByRole('dialog')).toBeVisible();
+
+ // The date picker should show a date (not "Pick a date")
+ await expect(
+ page.getByRole('dialog').getByRole('button', { name: 'Pick a date' })
+ ).not.toBeVisible();
+
+ // Click the clear button (X icon) to remove the expiration date
+ const clearButton = page
+ .getByRole('dialog')
+ .locator('[role="button"]')
+ .filter({ has: page.locator('svg.lucide-x') });
+ await expect(clearButton).toBeVisible();
+ await clearButton.click();
+
+ // The date picker should now show "Pick a date"
+ await expect(
+ page.getByRole('dialog').getByRole('button', { name: 'Pick a date' })
+ ).toBeVisible();
+
+ // The clear button should no longer be visible
+ await expect(clearButton).not.toBeVisible();
+
+ // Update the report and verify public_until is null
+ const [updateResponse] = await Promise.all([
+ page.waitForResponse(
+ (response) =>
+ response.url().includes('/reports/') &&
+ response.request().method() === 'PUT' &&
+ response.status() === 200
+ ),
+ page.getByRole('button', { name: 'Update Report' }).click(),
+ ]);
+ const updateBody = await updateResponse.json();
+ expect(updateBody.data.public_until).toBeNull();
+});
+
+test('test that date picker clear button is not visible when no date is set', async ({
+ page,
+ ctx,
+}) => {
+ const reportName = 'NoClearReport ' + Math.floor(Math.random() * 10000);
+
+ // Create a public report without an expiration date via API
+ await createReportViaApi(ctx, {
+ name: reportName,
+ is_public: true,
+ public_until: null,
+ });
+
+ // Go to shared reports and edit the report
+ await goToReportingShared(page);
+ await expect(page.getByText(reportName)).toBeVisible();
+
+ await page
+ .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) })
+ .click();
+ await page.getByRole('menuitem', { name: /^Edit Report/ }).click();
+ await expect(page.getByRole('dialog')).toBeVisible();
+
+ // The date picker should show "Pick a date"
+ await expect(
+ page.getByRole('dialog').getByRole('button', { name: 'Pick a date' })
+ ).toBeVisible();
+
+ // The clear button should NOT be visible
+ const clearButton = page
+ .getByRole('dialog')
+ .locator('[role="button"]')
+ .filter({ has: page.locator('svg.lucide-x') });
+ await expect(clearButton).not.toBeVisible();
+});
+
// ──────────────────────────────────────────────────
// Shared Report Cost Column Tests
// ──────────────────────────────────────────────────
diff --git a/e2e/utils/api.ts b/e2e/utils/api.ts
index 4f21240c..35998a0a 100644
--- a/e2e/utils/api.ts
+++ b/e2e/utils/api.ts
@@ -724,3 +724,43 @@ export async function createRunningTimeEntryWithStartViaApi(
const body = await response.json();
return body.data as { id: string; start: string; end: null; description: string };
}
+
+// ──────────────────────────────────────────────────
+// Reports
+// ──────────────────────────────────────────────────
+
+export async function createReportViaApi(
+ ctx: TestContext,
+ data: {
+ name: string;
+ is_public?: boolean;
+ public_until?: string | null;
+ }
+) {
+ const response = await ctx.request.post(
+ `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/reports`,
+ {
+ data: {
+ name: data.name,
+ description: '',
+ is_public: data.is_public ?? true,
+ public_until: data.public_until ?? null,
+ properties: {
+ start: '2024-01-01T00:00:00Z',
+ end: '2030-12-31T23:59:59Z',
+ group: 'project',
+ sub_group: 'project',
+ history_group: 'day',
+ },
+ },
+ }
+ );
+ expect(response.status()).toBe(201);
+ const body = await response.json();
+ return body.data as {
+ id: string;
+ name: string;
+ is_public: boolean;
+ public_until: string | null;
+ };
+}
diff --git a/resources/js/Components/Common/Report/ReportCreateModal.vue b/resources/js/Components/Common/Report/ReportCreateModal.vue
index 318554f9..5a3acdf0 100644
--- a/resources/js/Components/Common/Report/ReportCreateModal.vue
+++ b/resources/js/Components/Common/Report/ReportCreateModal.vue
@@ -111,7 +111,7 @@ async function submit() {