diff --git a/e2e/utils/api.ts b/e2e/utils/api.ts
index 8566e4b2..4f21240c 100644
--- a/e2e/utils/api.ts
+++ b/e2e/utils/api.ts
@@ -603,7 +603,124 @@ export async function getInvitationsViaApi(ctx: TestContext) {
const response = await ctx.request.get(
`${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invitations`
);
+
expect(response.status()).toBe(200);
const body = await response.json();
return body.data as Array<{ id: string; email: string; role: string }>;
}
+
+// ──────────────────────────────────────────────────
+// Timestamp-based time entry helpers
+// ──────────────────────────────────────────────────
+
+export async function createTimeEntryWithTimestampsViaApi(
+ ctx: TestContext,
+ data: {
+ description?: string;
+ start: string;
+ end: string;
+ projectId?: string | null;
+ taskId?: string | null;
+ tags?: string[];
+ billable?: boolean;
+ }
+) {
+ const response = await ctx.request.post(
+ `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
+ {
+ data: {
+ member_id: ctx.memberId,
+ start: data.start,
+ end: data.end,
+ description: data.description ?? '',
+ project_id: data.projectId ?? null,
+ task_id: data.taskId ?? null,
+ tags: data.tags ?? [],
+ billable: data.billable ?? false,
+ },
+ }
+ );
+ expect(response.status()).toBe(201);
+ const body = await response.json();
+ return body.data as { id: string; start: string; end: string; description: string };
+}
+
+// ──────────────────────────────────────────────────
+// User profile helpers
+// ──────────────────────────────────────────────────
+
+export async function updateUserProfileViaWeb(
+ page: Page,
+ settings: { timezone?: string; week_start?: string }
+) {
+ // Read user info from Inertia's data-page attribute on the root element
+ const userInfo = await page.evaluate(() => {
+ // Try Inertia's data-page attribute (stores initial page props as JSON)
+ const appEl = document.getElementById('app');
+ if (appEl) {
+ const dataPage = appEl.getAttribute('data-page');
+ if (dataPage) {
+ try {
+ const parsed = JSON.parse(dataPage);
+ const user = parsed?.props?.auth?.user;
+ if (user) {
+ return {
+ name: user.name,
+ email: user.email,
+ timezone: user.timezone,
+ week_start: user.week_start,
+ };
+ }
+ } catch {
+ // JSON parse failed
+ }
+ }
+ }
+ return null;
+ });
+ if (!userInfo) throw new Error('Could not read user info from Inertia data-page attribute');
+
+ const cookies = await page.context().cookies();
+ const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
+ const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
+
+ const response = await page.request.put(`${PLAYWRIGHT_BASE_URL}/user/profile-information`, {
+ headers: {
+ 'X-XSRF-TOKEN': xsrfToken,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ data: {
+ name: userInfo.name,
+ email: userInfo.email,
+ timezone: settings.timezone ?? userInfo.timezone,
+ week_start: settings.week_start ?? userInfo.week_start,
+ },
+ });
+ expect(response.status()).toBe(200);
+}
+
+// ──────────────────────────────────────────────────
+// Running time entry with specific start
+// ──────────────────────────────────────────────────
+
+export async function createRunningTimeEntryWithStartViaApi(
+ ctx: TestContext,
+ description: string,
+ start: string
+) {
+ const response = await ctx.request.post(
+ `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
+ {
+ data: {
+ member_id: ctx.memberId,
+ start,
+ description,
+ billable: false,
+ },
+ }
+ );
+ expect(response.status()).toBe(201);
+ const body = await response.json();
+ return body.data as { id: string; start: string; end: null; description: string };
+}
diff --git a/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue b/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue
index 9c202408..a706f81e 100644
--- a/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue
+++ b/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue
@@ -154,7 +154,7 @@ function closeAndFocusInput() {
v-model="currentTime"
placeholder="00:00:00"
data-testid="time_entry_time"
- class="w-[110px] lg:w-[120px] h-full text-text-primary py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base font-semibold tabular-nums bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition"
+ class="w-[110px] lg:w-[120px] h-full text-text-primary py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base font-semibold bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition"
type="text"
@focusin="openModalOnTab"
@click="openModalOnClick"
diff --git a/resources/js/packages/ui/src/index.ts b/resources/js/packages/ui/src/index.ts
index 94455145..9dcc9fa9 100644
--- a/resources/js/packages/ui/src/index.ts
+++ b/resources/js/packages/ui/src/index.ts
@@ -5,46 +5,58 @@ declare global {
}
}
-import * as money from './utils/money';
import * as color from './utils/color';
+import * as money from './utils/money';
import * as random from './utils/random';
import * as time from './utils/time';
-export { cn } from './utils/cn';
export { buttonVariants, type ButtonVariants } from './Buttons/index';
+export type {
+ CommandPaletteCommand,
+ CommandPaletteGroup,
+ EntitySearchResult,
+} from './CommandPalette/index';
+export type { FieldVariants } from './field/index';
+export type { CalendarSettings } from './FullCalendar/calendarSettings';
+export type { ActivityPeriod } from './FullCalendar/activityTypes';
+export { cn } from './utils/cn';
+import Badge from './Badge.vue';
+import Button from './Buttons/Button.vue';
import PrimaryButton from './Buttons/PrimaryButton.vue';
import SecondaryButton from './Buttons/SecondaryButton.vue';
-import Button from './Buttons/Button.vue';
-import TimeTrackerStartStop from './TimeTrackerStartStop.vue';
-import ProjectBadge from './Project/ProjectBadge.vue';
+import CardTitle from './CardTitle.vue';
+import Checkbox from './Input/Checkbox.vue';
+import InputLabel from './Input/InputLabel.vue';
+import TextInput from './Input/TextInput.vue';
import LoadingSpinner from './LoadingSpinner.vue';
import Modal from './Modal.vue';
-import TextInput from './Input/TextInput.vue';
-import InputLabel from './Input/InputLabel.vue';
-import TimeTrackerRunningInDifferentOrganizationOverlay from './TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
-import TimeTrackerControls from './TimeTracker/TimeTrackerControls.vue';
-import TimeTrackerMoreOptionsDropdown from './TimeTracker/TimeTrackerMoreOptionsDropdown.vue';
-import CardTitle from './CardTitle.vue';
-import Badge from './Badge.vue';
-import Checkbox from './Input/Checkbox.vue';
-import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue';
-import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue';
+import ProjectBadge from './Project/ProjectBadge.vue';
import TimeEntryCreateModal from './TimeEntry/TimeEntryCreateModal.vue';
import TimeEntryEditModal from './TimeEntry/TimeEntryEditModal.vue';
+import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue';
+import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue';
+import TimeTrackerControls from './TimeTracker/TimeTrackerControls.vue';
+import TimeTrackerMoreOptionsDropdown from './TimeTracker/TimeTrackerMoreOptionsDropdown.vue';
+import TimeTrackerRunningInDifferentOrganizationOverlay from './TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
+import TimeTrackerStartStop from './TimeTrackerStartStop.vue';
-import FullCalendarEventContent from './FullCalendar/FullCalendarEventContent.vue';
-import FullCalendarDayHeader from './FullCalendar/FullCalendarDayHeader.vue';
-import TimeEntryCalendar from './FullCalendar/TimeEntryCalendar.vue';
-import CalendarSettingsPopover from './FullCalendar/CalendarSettingsPopover.vue';
-import DateRangePicker from './Input/DateRangePicker.vue';
-import TimezoneMismatchModal from './TimezoneMismatchModal.vue';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip/index';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion/index';
-import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './popover/index';
-import { RangeCalendar } from './range-calendar/index';
+import {
+ Calendar,
+ CalendarCell,
+ CalendarCellTrigger,
+ CalendarGrid,
+ CalendarGridBody,
+ CalendarGridHead,
+ CalendarGridRow,
+ CalendarHeadCell,
+ CalendarHeader,
+ CalendarHeading,
+ CalendarNextButton,
+ CalendarPrevButton,
+} from './calendar/index';
import { CommandPalette } from './CommandPalette/index';
-import { Separator } from './separator/index';
import {
ContextMenu,
ContextMenuCheckboxItem,
@@ -61,6 +73,17 @@ import {
ContextMenuSubTrigger,
ContextMenuTrigger,
} from './context-menu/index';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogScrollContent,
+ DialogTitle,
+ DialogTrigger,
+} from './dialog/index';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -78,52 +101,6 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from './dropdown-menu/index';
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectItemText,
- SelectLabel,
- SelectScrollDownButton,
- SelectScrollUpButton,
- SelectSeparator,
- SelectTrigger,
- SelectValue,
-} from './select/index';
-import {
- NumberField,
- NumberFieldContent,
- NumberFieldDecrement,
- NumberFieldIncrement,
- NumberFieldInput,
-} from './number-field/index';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogScrollContent,
- DialogTitle,
- DialogTrigger,
-} from './dialog/index';
-import {
- Calendar,
- CalendarCell,
- CalendarCellTrigger,
- CalendarGrid,
- CalendarGridBody,
- CalendarGridHead,
- CalendarGridRow,
- CalendarHeadCell,
- CalendarHeader,
- CalendarHeading,
- CalendarNextButton,
- CalendarPrevButton,
-} from './calendar/index';
-import { Label } from './label/index';
import {
Field,
FieldContent,
@@ -137,71 +114,65 @@ import {
FieldTitle,
fieldVariants,
} from './field/index';
-export type { FieldVariants } from './field/index';
-export type { ActivityPeriod } from './FullCalendar/idleStatusPlugin';
-export type { CalendarSettings } from './FullCalendar/calendarSettings';
-export type {
- CommandPaletteCommand,
- CommandPaletteGroup,
- EntitySearchResult,
-} from './CommandPalette/index';
+import CalendarSettingsPopover from './FullCalendar/CalendarSettingsPopover.vue';
+import CalendarToolbar from './FullCalendar/CalendarToolbar.vue';
+import FullCalendarDayHeader from './FullCalendar/FullCalendarDayHeader.vue';
+import FullCalendarEventContent from './FullCalendar/FullCalendarEventContent.vue';
+import TimeEntryCalendar from './FullCalendar/TimeEntryCalendar.vue';
+import DateRangePicker from './Input/DateRangePicker.vue';
+import { Label } from './label/index';
+import {
+ NumberField,
+ NumberFieldContent,
+ NumberFieldDecrement,
+ NumberFieldIncrement,
+ NumberFieldInput,
+} from './number-field/index';
+import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from './popover/index';
+import { Progress } from './progress/index';
+import { RangeCalendar } from './range-calendar/index';
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectItemText,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+} from './select/index';
+import { Separator } from './separator/index';
+import TimezoneMismatchModal from './TimezoneMismatchModal.vue';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip/index';
export {
- money,
- color,
- random,
- time,
- Button,
- PrimaryButton,
- SecondaryButton,
- TimeTrackerStartStop,
- ProjectBadge,
- LoadingSpinner,
- Modal,
- TextInput,
- InputLabel,
- TimeTrackerRunningInDifferentOrganizationOverlay,
- TimeTrackerControls,
- TimeTrackerMoreOptionsDropdown,
- CardTitle,
- Badge,
- Checkbox,
- TimeEntryGroupedTable,
- TimeEntryMassActionRow,
- TimeEntryCreateModal,
- TimeEntryEditModal,
- FullCalendarEventContent,
- FullCalendarDayHeader,
- TimeEntryCalendar,
- CalendarSettingsPopover,
- DateRangePicker,
- TimezoneMismatchModal,
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
- Popover,
- PopoverContent,
- PopoverTrigger,
- PopoverAnchor,
- RangeCalendar,
+ Badge,
+ Button,
+ Calendar,
+ CalendarCell,
+ CalendarCellTrigger,
+ CalendarGrid,
+ CalendarGridBody,
+ CalendarGridHead,
+ CalendarGridRow,
+ CalendarHeadCell,
+ CalendarHeader,
+ CalendarHeading,
+ CalendarNextButton,
+ CalendarPrevButton,
+ CalendarSettingsPopover,
+ CalendarToolbar,
+ CardTitle,
+ Checkbox,
+ color,
CommandPalette,
- Separator,
- Field,
- FieldContent,
- FieldDescription,
- FieldError,
- FieldGroup,
- FieldLabel,
- FieldLegend,
- FieldSeparator,
- FieldSet,
- FieldTitle,
- fieldVariants,
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
@@ -216,6 +187,16 @@ export {
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
+ DateRangePicker,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogScrollContent,
+ DialogTitle,
+ DialogTrigger,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
@@ -231,6 +212,39 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
+ Field,
+ FieldContent,
+ FieldDescription,
+ FieldError,
+ FieldGroup,
+ FieldLabel,
+ FieldLegend,
+ FieldSeparator,
+ FieldSet,
+ FieldTitle,
+ fieldVariants,
+ FullCalendarDayHeader,
+ FullCalendarEventContent,
+ InputLabel,
+ Label,
+ LoadingSpinner,
+ Modal,
+ money,
+ NumberField,
+ NumberFieldContent,
+ NumberFieldDecrement,
+ NumberFieldIncrement,
+ NumberFieldInput,
+ Popover,
+ PopoverAnchor,
+ PopoverContent,
+ PopoverTrigger,
+ PrimaryButton,
+ Progress,
+ ProjectBadge,
+ random,
+ RangeCalendar,
+ SecondaryButton,
Select,
SelectContent,
SelectGroup,
@@ -242,31 +256,21 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
- NumberField,
- NumberFieldContent,
- NumberFieldDecrement,
- NumberFieldIncrement,
- NumberFieldInput,
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogScrollContent,
- DialogTitle,
- DialogTrigger,
- Calendar,
- CalendarCell,
- CalendarCellTrigger,
- CalendarGrid,
- CalendarGridBody,
- CalendarGridHead,
- CalendarGridRow,
- CalendarHeadCell,
- CalendarHeader,
- CalendarHeading,
- CalendarNextButton,
- CalendarPrevButton,
- Label,
+ Separator,
+ TextInput,
+ time,
+ TimeEntryCalendar,
+ TimeEntryCreateModal,
+ TimeEntryEditModal,
+ TimeEntryGroupedTable,
+ TimeEntryMassActionRow,
+ TimeTrackerControls,
+ TimeTrackerMoreOptionsDropdown,
+ TimeTrackerRunningInDifferentOrganizationOverlay,
+ TimeTrackerStartStop,
+ TimezoneMismatchModal,
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
};
diff --git a/resources/js/packages/ui/src/progress/Progress.vue b/resources/js/packages/ui/src/progress/Progress.vue
new file mode 100644
index 00000000..9fed4ca8
--- /dev/null
+++ b/resources/js/packages/ui/src/progress/Progress.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/resources/js/packages/ui/src/progress/index.ts b/resources/js/packages/ui/src/progress/index.ts
new file mode 100644
index 00000000..63f2d58f
--- /dev/null
+++ b/resources/js/packages/ui/src/progress/index.ts
@@ -0,0 +1 @@
+export { default as Progress } from './Progress.vue';