+ >
);
}
diff --git a/apps/web/src/features/tags/pages/tags-page.tsx b/apps/web/src/features/tags/pages/tags-page.tsx
index 59e70a8..eee68a3 100644
--- a/apps/web/src/features/tags/pages/tags-page.tsx
+++ b/apps/web/src/features/tags/pages/tags-page.tsx
@@ -1,8 +1,11 @@
import { useQuery } from "@tanstack/react-query";
+
+import { AppPage, AppPageHeader, AppSurface } from "@/components/app-page-shell";
import { authClient } from "@/lib/auth-client";
import { Skeleton } from "@open-learn/ui/components/skeleton";
-import { TagsTable } from "@/features/tags/components/tags-table";
-import { TAGS_COPY } from "@/features/tags/constants";
+
+import { TagsTable } from "../components/tags-table";
+import { TAGS_COPY } from "../constants";
export default function TagsPage() {
const sessionQuery = useQuery({
@@ -13,7 +16,7 @@ export default function TagsPage() {
if (sessionQuery.isPending) {
return (
-
+
@@ -28,9 +31,14 @@ export default function TagsPage() {
}
return (
-
-
{TAGS_COPY.pageTitle}
-
-
+
+
+
+
+
+
);
}
diff --git a/apps/web/src/features/tasks/pages/tasks-page.tsx b/apps/web/src/features/tasks/pages/tasks-page.tsx
index 9378705..a8f7c55 100644
--- a/apps/web/src/features/tasks/pages/tasks-page.tsx
+++ b/apps/web/src/features/tasks/pages/tasks-page.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
+import { AppPage, AppPageHeader, AppPageHeaderMeta } from "@/components/app-page-shell";
import { useProjectsQuery } from "@/features/projects/services/queries";
import { Badge } from "@open-learn/ui/components/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@open-learn/ui/components/tabs";
@@ -15,44 +16,51 @@ export function TasksPage() {
const projectsQuery = useProjectsQuery({ showArchived: false });
return (
-
-
-
-
{TASK_COPY.pageTitle}
-
{TASK_COPY.pageDescription}
-
+
+
+
+ Table
+
+ Kanban
+
+ New
+
+
+
+ }
+ >
+
+
-
-
- Table
-
- Kanban
-
- New
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
diff --git a/apps/web/src/features/time-tracker/components/activity-inline-editor.tsx b/apps/web/src/features/time-tracker/components/activity-inline-editor.tsx
index 7fe0b92..adc5698 100644
--- a/apps/web/src/features/time-tracker/components/activity-inline-editor.tsx
+++ b/apps/web/src/features/time-tracker/components/activity-inline-editor.tsx
@@ -23,7 +23,6 @@ import {
getEditableEntryValues,
} from "../utils/date-time";
import { ActivityReferenceInput } from "./activity-reference-input";
-import { CompactBillableToggle } from "./compact-billable-toggle";
import { CompactDatePicker } from "./compact-date-picker";
import { CompactProjectPicker } from "./compact-project-picker";
import { CompactTagPicker } from "./compact-tag-picker";
@@ -37,7 +36,6 @@ const activityEntrySchema = z
projectId: z.number().nullable(),
taskId: z.number().nullable(),
tagIds: z.array(z.number()),
- isBillable: z.boolean(),
})
.superRefine((value, ctx) => {
const startAt = combineDateAndTime(value.date, value.startTime);
@@ -91,7 +89,7 @@ export function ActivityInlineEditor({
projectId: value.projectId,
taskId: value.taskId,
tagIds: value.tagIds,
- isBillable: value.isBillable,
+ isBillable: false,
startAt: startAt.toISOString(),
endAt: endAt.toISOString(),
});
@@ -183,15 +181,6 @@ export function ActivityInlineEditor({
)}
-
- {(billableField) => (
-
- )}
-
-
{(field) => (
diff --git a/apps/web/src/features/time-tracker/components/activity-reference-input.tsx b/apps/web/src/features/time-tracker/components/activity-reference-input.tsx
index 78609e4..19e1ea5 100644
--- a/apps/web/src/features/time-tracker/components/activity-reference-input.tsx
+++ b/apps/web/src/features/time-tracker/components/activity-reference-input.tsx
@@ -1,7 +1,6 @@
import type { TaskListItem } from "@open-learn/api/modules/task/task.schema";
-import { Button } from "@open-learn/ui/components/button";
-import { ButtonGroup } from "@open-learn/ui/components/button-group";
+import { PencilLineIcon, CheckSquareIcon } from "lucide-react";
import { Input } from "@open-learn/ui/components/input";
import { CompactTaskPicker } from "./compact-task-picker";
@@ -32,32 +31,30 @@ export function ActivityReferenceInput({
onTaskChange,
tasks,
}: ActivityReferenceInputProps) {
+ const toggle = () => {
+ if (mode === "description") {
+ onModeChange("task");
+ description.onChange("");
+ } else {
+ onModeChange("description");
+ onTaskChange(null);
+ }
+ };
+
return (
-
-
-
-
-
+
+
{mode === "task" ? (
diff --git a/apps/web/src/features/time-tracker/components/activity-row.tsx b/apps/web/src/features/time-tracker/components/activity-row.tsx
index a8b5a8f..9090d3e 100644
--- a/apps/web/src/features/time-tracker/components/activity-row.tsx
+++ b/apps/web/src/features/time-tracker/components/activity-row.tsx
@@ -19,7 +19,6 @@ import {
isNextDay,
} from "../utils/date-time";
import { ActivityInlineEditor } from "./activity-inline-editor";
-import { BillableBadge } from "./billable-badge";
interface ActivityRowProps {
entry: TrackerEntry;
@@ -90,7 +89,6 @@ export function ActivityRow({
-
{entry.task ? {entry.task.displayKey} : null}
{entry.project ? {entry.project.name} : null}
{entry.tags.map((tag) => (
diff --git a/apps/web/src/features/time-tracker/components/billable-badge.tsx b/apps/web/src/features/time-tracker/components/billable-badge.tsx
deleted file mode 100644
index 4a74ac8..0000000
--- a/apps/web/src/features/time-tracker/components/billable-badge.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Badge } from "@open-learn/ui/components/badge";
-import { cn } from "@open-learn/ui/lib/utils";
-
-export function BillableBadge({
- isBillable,
- className,
-}: {
- isBillable: boolean;
- className?: string;
-}) {
- return (
-
- {isBillable ? "Billable" : "Non-billable"}
-
- );
-}
diff --git a/apps/web/src/features/time-tracker/components/calendar-active-timer-card.tsx b/apps/web/src/features/time-tracker/components/calendar-active-timer-card.tsx
index 6f22dd7..41ac737 100644
--- a/apps/web/src/features/time-tracker/components/calendar-active-timer-card.tsx
+++ b/apps/web/src/features/time-tracker/components/calendar-active-timer-card.tsx
@@ -50,7 +50,7 @@ export function CalendarActiveTimerCard({ activeEntry, now, range }: CalendarAct
-
-
-
- Billing
-
-
-
- {(billableField) => (
-
- )}
-
-
-
>
)}
diff --git a/apps/web/src/features/time-tracker/components/calendar-toolbar.tsx b/apps/web/src/features/time-tracker/components/calendar-toolbar.tsx
index 3efd5aa..92f24c1 100644
--- a/apps/web/src/features/time-tracker/components/calendar-toolbar.tsx
+++ b/apps/web/src/features/time-tracker/components/calendar-toolbar.tsx
@@ -1,5 +1,4 @@
import type { TrackerProject } from "@open-learn/api/modules/time-tracker/time-tracker.schema";
-import type { CalendarBillableFilter } from "../constants/calendar";
import type { CalendarViewKey } from "../utils/calendar";
import { Badge } from "@open-learn/ui/components/badge";
@@ -14,21 +13,15 @@ import {
} from "@open-learn/ui/components/select";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
-import {
- CALENDAR_BILLABLE_FILTER_OPTIONS,
- CALENDAR_COPY,
- CALENDAR_VIEW_OPTIONS,
-} from "../constants/calendar";
+import { CALENDAR_COPY, CALENDAR_VIEW_OPTIONS } from "../constants/calendar";
interface CalendarToolbarProps {
title: string;
view: CalendarViewKey;
projectFilter: string;
- billableFilter: CalendarBillableFilter;
projects: TrackerProject[];
onViewChange: (view: CalendarViewKey) => void;
onProjectFilterChange: (value: string) => void;
- onBillableFilterChange: (value: CalendarBillableFilter) => void;
onToday: () => void;
onPrevious: () => void;
onNext: () => void;
@@ -38,11 +31,9 @@ export function CalendarToolbar({
title,
view,
projectFilter,
- billableFilter,
projects,
onViewChange,
onProjectFilterChange,
- onBillableFilterChange,
onToday,
onPrevious,
onNext,
@@ -103,22 +94,6 @@ export function CalendarToolbar({
))}
-
-
);
diff --git a/apps/web/src/features/time-tracker/components/charts.tsx b/apps/web/src/features/time-tracker/components/charts.tsx
new file mode 100644
index 0000000..109cb71
--- /dev/null
+++ b/apps/web/src/features/time-tracker/components/charts.tsx
@@ -0,0 +1,224 @@
+import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis } from "recharts";
+
+import {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+ type ChartConfig,
+} from "@open-learn/ui/components/chart";
+
+import { formatMetricDuration, formatProjectShare } from "../utils/dashboard";
+
+// ─── Shared config & types ────────────────────────────────────────────────────
+
+const dailyChartConfig = {
+ totalHours: {
+ label: "Tracked hours",
+ color: "hsl(174 84% 32%)",
+ },
+} satisfies ChartConfig;
+
+const projectChartConfig = {
+ hours: {
+ label: "Hours",
+ color: "hsl(174 84% 32%)",
+ },
+} satisfies ChartConfig;
+
+export interface DailyEntry {
+ label: string;
+ totalHours: number;
+}
+
+export interface PieChartItem {
+ name: string;
+ seconds: number;
+ hours?: number;
+ percentage: number;
+ fill: string;
+}
+
+// ─── Shared sub-components ────────────────────────────────────────────────────
+
+function ShareBar({ percentage, color }: { percentage: number; color: string }) {
+ return (
+
+ );
+}
+
+// ─── TrackerBarChart ──────────────────────────────────────────────────────────
+
+interface TrackerBarChartProps {
+ daily: DailyEntry[];
+ /** Container height in px. Default: 300 */
+ height?: number;
+ /** Gap between bars. Default: 10 */
+ barGap?: number;
+ /** Maximum bar width in px. Default: 28 */
+ maxBarSize?: number;
+ /** Minimum tick gap on X axis. Default: 18 */
+ minTickGap?: number;
+ /** Y axis width in px. Default: 42 */
+ yAxisWidth?: number;
+}
+
+export function TrackerBarChart({
+ daily,
+ height = 300,
+ barGap = 10,
+ maxBarSize = 28,
+ minTickGap = 18,
+ yAxisWidth = 42,
+}: TrackerBarChartProps) {
+ return (
+
+
+
+
+ `${value}h`}
+ />
+ } />
+
+
+
+ );
+}
+
+// ─── DashboardPieChart ────────────────────────────────────────────────────────
+
+export function DashboardPieChart({
+ breakdownItems,
+ totalSeconds,
+}: {
+ breakdownItems: PieChartItem[];
+ totalSeconds: number;
+}) {
+ return (
+
+
+
+
+ } />
+
+ {breakdownItems.map((item) => (
+ |
+ ))}
+
+
+
+
+ {formatMetricDuration(totalSeconds)}
+
+
+
+
+ {breakdownItems.map((item) => (
+
+
{item.name}
+
+ {formatMetricDuration(item.seconds)}
+
+
+
+ {formatProjectShare(item.percentage)}
+
+
+ ))}
+
+
+ );
+}
+
+// ─── ReportsPieChart ──────────────────────────────────────────────────────────
+
+const reportsPieConfig = {
+ percentage: {
+ label: "Project share",
+ color: "hsl(174 84% 32%)",
+ },
+} satisfies ChartConfig;
+
+export function ReportsPieChart({
+ projects,
+ totalSeconds,
+}: {
+ projects: PieChartItem[];
+ totalSeconds: number;
+}) {
+ return (
+
+
+
+
+ } />
+
+ {projects.map((project) => (
+ |
+ ))}
+
+
+
+
+ {formatMetricDuration(totalSeconds)}
+ total tracked
+
+
+
+
+ {projects.map((project) => (
+
+
+
+ {project.name}
+
+
{formatMetricDuration(project.seconds)}
+
{project.percentage.toFixed(1)}%
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/features/time-tracker/components/compact-billable-toggle.tsx b/apps/web/src/features/time-tracker/components/compact-billable-toggle.tsx
deleted file mode 100644
index 3352a4d..0000000
--- a/apps/web/src/features/time-tracker/components/compact-billable-toggle.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Switch } from "@open-learn/ui/components/switch";
-import { cn } from "@open-learn/ui/lib/utils";
-
-import { BillableBadge } from "./billable-badge";
-
-export function CompactBillableToggle({
- checked,
- onCheckedChange,
- className,
-}: {
- checked: boolean;
- onCheckedChange: (checked: boolean) => void;
- className?: string;
-}) {
- return (
-
-
-
-
- );
-}
diff --git a/apps/web/src/features/time-tracker/components/dashboard-charts.tsx b/apps/web/src/features/time-tracker/components/dashboard-charts.tsx
deleted file mode 100644
index 322cb04..0000000
--- a/apps/web/src/features/time-tracker/components/dashboard-charts.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis } from "recharts";
-
-import {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- type ChartConfig,
-} from "@open-learn/ui/components/chart";
-
-import { formatMetricDuration, formatProjectShare } from "../utils/dashboard";
-
-const dailyChartConfig = {
- totalHours: {
- label: "Tracked hours",
- color: "hsl(174 84% 32%)",
- },
- billableHours: {
- label: "Billable hours",
- color: "hsl(200 78% 46%)",
- },
-} satisfies ChartConfig;
-
-const projectChartConfig = {
- hours: {
- label: "Hours",
- color: "hsl(174 84% 32%)",
- },
-} satisfies ChartConfig;
-
-interface DailyEntry {
- label: string;
- totalHours: number;
- billableHours: number;
-}
-
-interface BreakdownItem {
- name: string;
- seconds: number;
- hours: number;
- percentage: number;
- fill: string;
-}
-
-function ShareBar({ percentage, color }: { percentage: number; color: string }) {
- return (
-
- );
-}
-
-export function DashboardBarChart({ daily }: { daily: DailyEntry[] }) {
- return (
-
-
-
-
- `${value}h`}
- />
- } />
-
-
-
-
- );
-}
-
-export function DashboardPieChart({
- breakdownItems,
- totalSeconds,
-}: {
- breakdownItems: BreakdownItem[];
- totalSeconds: number;
-}) {
- return (
-
-
-
-
- } />
-
- {breakdownItems.map((item) => (
- |
- ))}
-
-
-
-
- {formatMetricDuration(totalSeconds)}
-
-
-
-
- {breakdownItems.map((item) => (
-
-
{item.name}
-
- {formatMetricDuration(item.seconds)}
-
-
-
- {formatProjectShare(item.percentage)}
-
-
- ))}
-
-
- );
-}
diff --git a/apps/web/src/features/time-tracker/components/entry-metadata-fields.tsx b/apps/web/src/features/time-tracker/components/entry-metadata-fields.tsx
index bffa3a7..51d723c 100644
--- a/apps/web/src/features/time-tracker/components/entry-metadata-fields.tsx
+++ b/apps/web/src/features/time-tracker/components/entry-metadata-fields.tsx
@@ -6,7 +6,6 @@ import type { TrackerOverviewRange } from "../utils/date-time";
import { Field, FieldError, FieldGroup, FieldLabel } from "@open-learn/ui/components/field";
import { Input } from "@open-learn/ui/components/input";
-import { Switch } from "@open-learn/ui/components/switch";
import { ProjectPicker } from "./project-picker";
import { TagPicker } from "./tag-picker";
@@ -25,8 +24,6 @@ interface EntryMetadataFieldsProps {
tagIds: number[];
onTagIdsChange: (value: number[]) => void;
tags: TrackerTag[];
- isBillable: boolean;
- onBillableChange: (value: boolean) => void;
range: TrackerOverviewRange;
}
@@ -38,8 +35,6 @@ export function EntryMetadataFields({
tagIds,
onTagIdsChange,
tags,
- isBillable,
- onBillableChange,
range,
}: EntryMetadataFieldsProps) {
return (
@@ -65,11 +60,6 @@ export function EntryMetadataFields({
/>
-
-
- Billable
-
-
);
}
diff --git a/apps/web/src/features/time-tracker/components/manual-entry-form.tsx b/apps/web/src/features/time-tracker/components/manual-entry-form.tsx
index 63df8db..1d0e594 100644
--- a/apps/web/src/features/time-tracker/components/manual-entry-form.tsx
+++ b/apps/web/src/features/time-tracker/components/manual-entry-form.tsx
@@ -21,7 +21,6 @@ import {
getDefaultManualValues,
} from "../utils/date-time";
import { ActivityReferenceInput } from "./activity-reference-input";
-import { CompactBillableToggle } from "./compact-billable-toggle";
import { CompactDatePicker } from "./compact-date-picker";
import { CompactProjectPicker } from "./compact-project-picker";
import { CompactTagPicker } from "./compact-tag-picker";
@@ -35,7 +34,6 @@ const manualEntrySchema = z
projectId: z.number().nullable(),
taskId: z.number().nullable(),
tagIds: z.array(z.number()),
- isBillable: z.boolean(),
})
.superRefine((value, ctx) => {
const startAt = combineDateAndTime(value.date, value.startTime);
@@ -76,7 +74,7 @@ export function ManualEntryForm({ projects, tasks, tags, range }: ManualEntryFor
projectId: value.projectId,
taskId: value.taskId,
tagIds: value.tagIds,
- isBillable: value.isBillable,
+ isBillable: false,
startAt: startAt.toISOString(),
endAt: endAt.toISOString(),
});
@@ -165,15 +163,6 @@ export function ManualEntryForm({ projects, tasks, tags, range }: ManualEntryFor
)}
-
- {(billableField) => (
-
- )}
-
-
{(field) => {
diff --git a/apps/web/src/features/time-tracker/components/reports-charts.tsx b/apps/web/src/features/time-tracker/components/reports-charts.tsx
deleted file mode 100644
index 3bffc01..0000000
--- a/apps/web/src/features/time-tracker/components/reports-charts.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis } from "recharts";
-
-import {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- type ChartConfig,
-} from "@open-learn/ui/components/chart";
-
-import { formatMetricDuration } from "../utils/dashboard";
-
-const dailyChartConfig = {
- totalHours: {
- label: "Tracked hours",
- color: "hsl(174 84% 32%)",
- },
- billableHours: {
- label: "Billable hours",
- color: "hsl(200 78% 46%)",
- },
-} satisfies ChartConfig;
-
-const projectChartConfig = {
- percentage: {
- label: "Project share",
- color: "hsl(174 84% 32%)",
- },
-} satisfies ChartConfig;
-
-interface DailyEntry {
- label: string;
- totalHours: number;
- billableHours: number;
-}
-
-interface ProjectEntry {
- name: string;
- seconds: number;
- percentage: number;
- fill: string;
-}
-
-export function ReportsBarChart({ daily }: { daily: DailyEntry[] }) {
- return (
-
-
-
-
- `${value}h`}
- />
- } />
-
-
-
-
- );
-}
-
-export function ReportsPieChart({
- projects,
- totalSeconds,
-}: {
- projects: ProjectEntry[];
- totalSeconds: number;
-}) {
- return (
-
-
-
-
- } />
-
- {projects.map((project) => (
- |
- ))}
-
-
-
-
- {formatMetricDuration(totalSeconds)}
- total tracked
-
-
-
-
- {projects.map((project) => (
-
-
-
- {project.name}
-
-
{formatMetricDuration(project.seconds)}
-
{project.percentage.toFixed(1)}%
-
- ))}
-
-
- );
-}
diff --git a/apps/web/src/features/time-tracker/components/timer-entry-form.tsx b/apps/web/src/features/time-tracker/components/timer-entry-form.tsx
index 13bf808..d1fabc4 100644
--- a/apps/web/src/features/time-tracker/components/timer-entry-form.tsx
+++ b/apps/web/src/features/time-tracker/components/timer-entry-form.tsx
@@ -8,18 +8,14 @@ import type { TrackerOverviewRange } from "../utils/date-time";
import { useForm } from "@tanstack/react-form";
import { useEffect, useMemo, useState } from "react";
-import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { z } from "zod";
import { Button } from "@open-learn/ui/components/button";
-import { Collapsible, CollapsibleContent } from "@open-learn/ui/components/collapsible";
import { FieldError } from "@open-learn/ui/components/field";
import { useStartTimer, useStopTimer, useUpdateActiveTimer } from "../services/mutations";
import { formatDuration, getElapsedSeconds, getTimerFormValues } from "../utils/date-time";
import { getCompatibleTaskId } from "../utils/task-reference";
import { ActivityReferenceInput } from "./activity-reference-input";
-import { BillableBadge } from "./billable-badge";
-import { CompactBillableToggle } from "./compact-billable-toggle";
import { CompactProjectPicker } from "./compact-project-picker";
import { CompactTagPicker } from "./compact-tag-picker";
@@ -28,7 +24,6 @@ const timerFormSchema = z.object({
projectId: z.number().nullable(),
taskId: z.number().nullable(),
tagIds: z.array(z.number()),
- isBillable: z.boolean(),
});
interface TimerEntryFormProps {
@@ -41,7 +36,6 @@ interface TimerEntryFormProps {
export function TimerEntryForm({ activeEntry, projects, tasks, tags, range }: TimerEntryFormProps) {
const [now, setNow] = useState(() => new Date());
- const [detailsOpen, setDetailsOpen] = useState(false);
const [activityMode, setActivityMode] = useState<"description" | "task">(
activeEntry?.task ? "task" : "description",
);
@@ -59,7 +53,7 @@ export function TimerEntryForm({ activeEntry, projects, tasks, tags, range }: Ti
projectId: value.projectId,
taskId: value.taskId,
tagIds: value.tagIds,
- isBillable: value.isBillable,
+ isBillable: false,
});
await stopTimer.mutateAsync({ entryId: activeEntry.id });
return;
@@ -70,11 +64,10 @@ export function TimerEntryForm({ activeEntry, projects, tasks, tags, range }: Ti
projectId: value.projectId,
taskId: value.taskId,
tagIds: value.tagIds,
- isBillable: value.isBillable,
+ isBillable: false,
});
form.reset(getTimerFormValues(null));
setActivityMode("description");
- setDetailsOpen(false);
},
});
@@ -102,134 +95,107 @@ export function TimerEntryForm({ activeEntry, projects, tasks, tags, range }: Ti
}, [activeEntry, now]);
return (
-
-
- {(descriptionField) => {
- const isInvalid =
- descriptionField.state.meta.isTouched && !descriptionField.state.meta.isValid;
-
- return (
- <>
-
-
state.values.projectId}>
- {(projectId) => (
-
- {(taskField) => (
-
- )}
-
- )}
-
-
- {activeEntry ?
: null}
-
-
- {elapsedLabel}
-
-
-
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => (
-
- )}
-
-
-
-
+
+ {(descriptionField) => {
+ const isInvalid =
+ descriptionField.state.meta.isTouched && !descriptionField.state.meta.isValid;
+
+ return (
+ <>
+
+
state.values.projectId}>
+ {(projectId) => (
+
+ {(taskField) => (
+
+ )}
+
+ )}
+
+
+
+ {(projectField) => (
+
+ {(taskField) => (
+ {
+ projectField.handleChange(projectId);
+ taskField.handleChange(
+ getCompatibleTaskId(tasks, taskField.state.value, projectId),
+ );
+ }}
+ projects={projects}
+ range={range}
+ />
+ )}
+
+ )}
+
-
-
-
- >
- );
- }}
-
-
-
-
-
- {(projectField) => (
-
- {(taskField) => (
- {
- projectField.handleChange(projectId);
- taskField.handleChange(
- getCompatibleTaskId(tasks, taskField.state.value, projectId),
- );
- }}
- projects={projects}
+
+ {(tagField) => (
+
)}
- )}
-
-
-
- {(tagField) => (
-
- )}
-
-
-
- {(billableField) => (
-
- )}
-
-
-
-
-
+
+
+ {elapsedLabel}
+
+
+
({ isSubmitting: state.isSubmitting })}>
+ {({ isSubmitting }) => (
+
+ )}
+
+
+
+
+
+
+ >
+ );
+ }}
+
+
);
}
diff --git a/apps/web/src/features/time-tracker/constants/calendar.ts b/apps/web/src/features/time-tracker/constants/calendar.ts
index f04849c..a57ad1e 100644
--- a/apps/web/src/features/time-tracker/constants/calendar.ts
+++ b/apps/web/src/features/time-tracker/constants/calendar.ts
@@ -1,7 +1,5 @@
import type { CalendarViewKey } from "../utils/calendar";
-export type CalendarBillableFilter = "all" | "billable" | "non-billable";
-
export const CALENDAR_COPY = {
pageTitle: "Calendar",
pageDescription: "Browse and manage tracked time in a calendar view.",
@@ -17,12 +15,3 @@ export const CALENDAR_VIEW_OPTIONS: Array<{ label: string; value: CalendarViewKe
{ label: "Day", value: "day" },
{ label: "Month", value: "month" },
];
-
-export const CALENDAR_BILLABLE_FILTER_OPTIONS: Array<{
- label: string;
- value: CalendarBillableFilter;
-}> = [
- { label: "All time", value: "all" },
- { label: "Billable", value: "billable" },
- { label: "Non-billable", value: "non-billable" },
-];
diff --git a/apps/web/src/features/time-tracker/constants/index.ts b/apps/web/src/features/time-tracker/constants/index.ts
index d33f9ff..8d02454 100644
--- a/apps/web/src/features/time-tracker/constants/index.ts
+++ b/apps/web/src/features/time-tracker/constants/index.ts
@@ -1,6 +1,6 @@
export const TRACKER_COPY = {
pageTitle: "Track your time",
- pageDescription: "Capture billable and non-billable work with a timer or manual entry.",
+ pageDescription: "Capture your work with a timer or manual entry.",
timerTab: "Timer",
manualTab: "Manual",
activeTimerTitle: "Active timer",
diff --git a/apps/web/src/features/time-tracker/pages/calendar-page.tsx b/apps/web/src/features/time-tracker/pages/calendar-page.tsx
index 60aac07..91e7163 100644
--- a/apps/web/src/features/time-tracker/pages/calendar-page.tsx
+++ b/apps/web/src/features/time-tracker/pages/calendar-page.tsx
@@ -1,9 +1,12 @@
import type { TrackerEntry } from "@open-learn/api/modules/time-tracker/time-tracker.schema";
import type { SlotInfo } from "react-big-calendar";
-import type { CalendarBillableFilter } from "../constants/calendar";
import type { CalendarEntryEvent, CalendarSheetMode, CalendarViewKey } from "../utils/calendar";
import { useEffect, useMemo, useState } from "react";
+import { CalendarClockIcon } from "lucide-react";
+
+import { AppPage, AppPageHeader, AppPageHeaderMeta } from "@/components/app-page-shell";
+import { useTasksQuery } from "@/features/tasks/services/queries";
import { Button } from "@open-learn/ui/components/button";
import {
Empty,
@@ -14,16 +17,14 @@ import {
EmptyTitle,
} from "@open-learn/ui/components/empty";
import { Skeleton } from "@open-learn/ui/components/skeleton";
-import { CalendarClockIcon } from "lucide-react";
-import { useTasksQuery } from "@/features/tasks/services/queries";
import { CALENDAR_COPY } from "../constants/calendar";
import { CalendarActiveTimerCard } from "../components/calendar-active-timer-card";
import { CalendarEntrySheet } from "../components/calendar-entry-sheet";
import { CalendarToolbar } from "../components/calendar-toolbar";
import { TrackerCalendar } from "../components/tracker-calendar";
-import { useTrackerOverviewQuery } from "../services/queries";
import { useUpdateEntry } from "../services/mutations";
+import { useTrackerOverviewQuery } from "../services/queries";
import {
getCalendarRange,
getCalendarViewTitle,
@@ -42,7 +43,6 @@ export default function CalendarPage() {
const [view, setView] = useState("week");
const [focusedDate, setFocusedDate] = useState(() => new Date());
const [projectFilter, setProjectFilter] = useState("all");
- const [billableFilter, setBillableFilter] = useState("all");
const [sheetState, setSheetState] = useState(null);
const [now, setNow] = useState(() => new Date());
@@ -65,18 +65,9 @@ export default function CalendarPage() {
const entries = trackerOverview.data?.entries ?? [];
return entries.filter((entry) => {
- const matchesProject =
- projectFilter === "all" ? true : String(entry.project?.id ?? "") === projectFilter;
- const matchesBillable =
- billableFilter === "all"
- ? true
- : billableFilter === "billable"
- ? entry.isBillable
- : !entry.isBillable;
-
- return matchesProject && matchesBillable;
+ return projectFilter === "all" ? true : String(entry.project?.id ?? "") === projectFilter;
});
- }, [billableFilter, projectFilter, trackerOverview.data?.entries]);
+ }, [projectFilter, trackerOverview.data?.entries]);
const visibleEvents = useMemo(
() =>
@@ -85,16 +76,12 @@ export default function CalendarPage() {
activeEntry:
trackerOverview.data?.activeEntry &&
(projectFilter === "all" ||
- String(trackerOverview.data.activeEntry.project?.id ?? "") === projectFilter) &&
- (billableFilter === "all" ||
- (billableFilter === "billable"
- ? trackerOverview.data.activeEntry.isBillable
- : !trackerOverview.data.activeEntry.isBillable))
+ String(trackerOverview.data.activeEntry.project?.id ?? "") === projectFilter)
? trackerOverview.data.activeEntry
: null,
now,
}),
- [billableFilter, filteredEntries, now, projectFilter, trackerOverview.data?.activeEntry],
+ [filteredEntries, now, projectFilter, trackerOverview.data?.activeEntry],
);
const title = useMemo(() => getCalendarViewTitle(view, focusedDate), [focusedDate, view]);
@@ -181,21 +168,35 @@ export default function CalendarPage() {
const isLoading = (trackerOverview.isLoading && !trackerOverview.data) || tasksQuery.isLoading;
return (
-
-
-
{CALENDAR_COPY.pageTitle}
-
{CALENDAR_COPY.pageDescription}
-
+
+
+ String(project.id) === projectFilter,
+ )?.name ?? "Filtered"),
+ },
+ ]}
+ />
+
setFocusedDate(new Date())}
onPrevious={() => setFocusedDate((current) => shiftCalendarDate(view, current, -1))}
onNext={() => setFocusedDate((current) => shiftCalendarDate(view, current, 1))}
@@ -271,6 +272,6 @@ export default function CalendarPage() {
}
}}
/>
-
+
);
}
diff --git a/apps/web/src/features/time-tracker/pages/reports-page.tsx b/apps/web/src/features/time-tracker/pages/reports-page.tsx
index 9b3abc5..e933757 100644
--- a/apps/web/src/features/time-tracker/pages/reports-page.tsx
+++ b/apps/web/src/features/time-tracker/pages/reports-page.tsx
@@ -13,6 +13,13 @@ import {
} from "lucide-react";
import { toast } from "sonner";
+import {
+ AppPage,
+ AppPageHeader,
+ AppPageHeaderMeta,
+ AppSurface,
+ AppSurfaceHeader,
+} from "@/components/app-page-shell";
import { Badge } from "@open-learn/ui/components/badge";
import { Button } from "@open-learn/ui/components/button";
import {
@@ -63,18 +70,12 @@ import {
} from "../utils/reports";
const ReportsBarChart = lazy(() =>
- import("../components/reports-charts").then((m) => ({ default: m.ReportsBarChart })),
+ import("../components/charts").then((module) => ({ default: module.TrackerBarChart })),
);
const ReportsPieChart = lazy(() =>
- import("../components/reports-charts").then((m) => ({ default: m.ReportsPieChart })),
+ import("../components/charts").then((module) => ({ default: module.ReportsPieChart })),
);
-const billableOptions = [
- { value: "all", label: "Billability" },
- { value: "billable", label: "Billable only" },
- { value: "non-billable", label: "Non-billable" },
-] as const;
-
const initialRange = getReportPresetRange("last-month");
function addDays(date: Date, days: number) {
@@ -83,16 +84,11 @@ function addDays(date: Date, days: number) {
return result;
}
-// ─── Reducer ──────────────────────────────────────────────────────────────────
-
-type BillableFilter = (typeof billableOptions)[number]["value"];
-
interface FiltersState {
preset: ReportPresetKey;
fromInput: string;
toInput: string;
projectFilter: string;
- billableFilter: BillableFilter;
searchValue: string;
}
@@ -101,7 +97,6 @@ type FiltersAction =
| { type: "SET_CUSTOM_FROM"; fromInput: string }
| { type: "SET_CUSTOM_TO"; toInput: string }
| { type: "SET_PROJECT_FILTER"; projectFilter: string }
- | { type: "SET_BILLABLE_FILTER"; billableFilter: BillableFilter }
| { type: "SET_SEARCH"; searchValue: string }
| { type: "RESET_FILTERS" }
| { type: "SHIFT_RANGE"; fromInput: string; toInput: string };
@@ -121,12 +116,10 @@ function filtersReducer(state: FiltersState, action: FiltersAction): FiltersStat
return { ...state, preset: "custom", toInput: action.toInput };
case "SET_PROJECT_FILTER":
return { ...state, projectFilter: action.projectFilter };
- case "SET_BILLABLE_FILTER":
- return { ...state, billableFilter: action.billableFilter };
case "SET_SEARCH":
return { ...state, searchValue: action.searchValue };
case "RESET_FILTERS":
- return { ...state, projectFilter: "all", billableFilter: "all", searchValue: "" };
+ return { ...state, projectFilter: "all", searchValue: "" };
case "SHIFT_RANGE":
return { ...state, preset: "custom", fromInput: action.fromInput, toInput: action.toInput };
default:
@@ -139,15 +132,12 @@ const initialFiltersState: FiltersState = {
fromInput: initialRange.fromInput,
toInput: initialRange.toInput,
projectFilter: "all",
- billableFilter: "all",
searchValue: "",
};
-// ─── Page ─────────────────────────────────────────────────────────────────────
-
export default function ReportsPage() {
const [filters, dispatch] = useReducer(filtersReducer, initialFiltersState);
- const { preset, fromInput, toInput, projectFilter, billableFilter, searchValue } = filters;
+ const { preset, fromInput, toInput, projectFilter, searchValue } = filters;
const resolvedRange = useMemo(() => resolveReportRange(fromInput, toInput), [fromInput, toInput]);
const rangeLabel = useMemo(
@@ -164,14 +154,6 @@ export default function ReportsPage() {
return false;
}
- if (billableFilter === "billable" && !entry.isBillable) {
- return false;
- }
-
- if (billableFilter === "non-billable" && entry.isBillable) {
- return false;
- }
-
if (!query) {
return true;
}
@@ -182,7 +164,7 @@ export default function ReportsPage() {
return haystack.includes(query);
});
- }, [billableFilter, projectFilter, searchValue, trackerOverview.data?.entries]);
+ }, [projectFilter, searchValue, trackerOverview.data?.entries]);
const metrics = useMemo(
() =>
@@ -277,92 +259,97 @@ export default function ReportsPage() {
}
return (
-
-
-
-
-
- Summary
-
-
- Detailed
-
-
- Weekly
-
-
- Shared
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+ handleExport("csv")}>
+
+ Export CSV
+
+ handleExport("excel")}>
+
+ Export Excel
+
+ handleExport("json")}>
+
+ Export JSON
+
+
+
+ >
+ }
+ >
+
option.key === preset)?.label ?? "Custom",
+ },
+ { label: "Sessions", value: String(metrics.totalSessions) },
+ { label: "Projects", value: String(metrics.projectCount) },
+ ]}
+ />
+
+
+
+ shiftRange(1)}
- aria-label="Next period"
+ className="h-9 rounded-none px-3"
+ onClick={resetFilters}
>
-
+ Reset filters
-
-
-
-
-
-
-
- handleExport("csv")}>
-
- Export CSV
-
- handleExport("excel")}>
-
- Export Excel
-
- handleExport("json")}>
-
- Export JSON
-
-
-
-
-
+ }
+ />
-
-
+
-
-
- dispatch({ type: "SET_BILLABLE_FILTER", billableFilter: value as BillableFilter })
- }
- ariaLabel="Filter by billability"
- >
- {billableOptions.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
-
-
-
+
+
-
-
@@ -498,7 +455,14 @@ export default function ReportsPage() {
<>
}>
-
+
@@ -526,7 +490,6 @@ export default function ReportsPage() {
Date
Duration
Project
-
Billable
@@ -544,18 +507,6 @@ export default function ReportsPage() {
{row.dateLabel}
{row.duration}
{row.projectName}
-
-
- {row.billableLabel}
-
-
))}
@@ -591,7 +542,7 @@ export default function ReportsPage() {
)}
-
+
);
}
diff --git a/apps/web/src/features/time-tracker/pages/time-tracker-page.tsx b/apps/web/src/features/time-tracker/pages/time-tracker-page.tsx
index 56997fb..bce1cb4 100644
--- a/apps/web/src/features/time-tracker/pages/time-tracker-page.tsx
+++ b/apps/web/src/features/time-tracker/pages/time-tracker-page.tsx
@@ -1,13 +1,26 @@
import { useMemo, useState } from "react";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@open-learn/ui/components/tabs";
+import {
+ AppPage,
+ AppPageHeader,
+ AppPageHeaderMeta,
+ AppSurface,
+ AppSurfaceHeader,
+} from "@/components/app-page-shell";
import { useTasksQuery } from "@/features/tasks/services/queries";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@open-learn/ui/components/tabs";
+
import { ActivityHistoryList } from "../components/activity-history-list";
import { ManualEntryForm } from "../components/manual-entry-form";
import { TimerEntryForm } from "../components/timer-entry-form";
import { TRACKER_COPY } from "../constants";
import { useTrackerOverviewQuery } from "../services/queries";
-import { getTrackerOverviewRange } from "../utils/date-time";
+import {
+ formatDuration,
+ getElapsedSeconds,
+ getEntryDurationSeconds,
+ getTrackerOverviewRange,
+} from "../utils/date-time";
export default function TimeTrackerPage() {
const [mode, setMode] = useState("timer");
@@ -16,51 +29,100 @@ export default function TimeTrackerPage() {
const trackerOverview = useTrackerOverviewQuery(range);
const tasksQuery = useTasksQuery();
- return (
-
-
-
{TRACKER_COPY.pageTitle}
-
{TRACKER_COPY.pageDescription}
-
+ const entries = trackerOverview.data?.entries ?? [];
+ const activeEntry = trackerOverview.data?.activeEntry ?? null;
+ const projects = trackerOverview.data?.projects ?? [];
+ const tags = trackerOverview.data?.tags ?? [];
+
+ const todayTotal = useMemo(() => {
+ const todayKey = new Date().toDateString();
+ const completedToday = entries.reduce((total, entry) => {
+ return new Date(entry.startAt).toDateString() === todayKey
+ ? total + getEntryDurationSeconds(entry)
+ : total;
+ }, 0);
-
-
-
- {TRACKER_COPY.timerTab}
- {TRACKER_COPY.manualTab}
-
-
+ if (!activeEntry || new Date(activeEntry.startAt).toDateString() !== todayKey) {
+ return completedToday;
+ }
-
-
+
+
+ {TRACKER_COPY.timerTab}
+ {TRACKER_COPY.manualTab}
+
+ }
+ >
+
-
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/src/features/time-tracker/pages/tracker-dashboard-page.tsx b/apps/web/src/features/time-tracker/pages/tracker-dashboard-page.tsx
index d919d03..66bf290 100644
--- a/apps/web/src/features/time-tracker/pages/tracker-dashboard-page.tsx
+++ b/apps/web/src/features/time-tracker/pages/tracker-dashboard-page.tsx
@@ -1,6 +1,7 @@
import { lazy, Suspense, useMemo, useState, type ReactNode } from "react";
import { ActivityIcon, CalendarRangeIcon } from "lucide-react";
+import { AppPage, AppPageHeader, AppPageHeaderMeta } from "@/components/app-page-shell";
import { Badge } from "@open-learn/ui/components/badge";
import {
Card,
@@ -32,17 +33,18 @@ import {
import { getEntryDescriptionLabel, getElapsedSeconds } from "../utils/date-time";
const DashboardBarChart = lazy(() =>
- import("../components/dashboard-charts").then((m) => ({ default: m.DashboardBarChart })),
+ import("../components/charts").then((module) => ({
+ default: module.TrackerBarChart,
+ })),
);
const DashboardPieChart = lazy(() =>
- import("../components/dashboard-charts").then((m) => ({ default: m.DashboardPieChart })),
+ import("../components/charts").then((module) => ({
+ default: module.DashboardPieChart,
+ })),
);
-type BreakdownMode = "project" | "billability";
-
export default function TrackerDashboardPage() {
const [rangeKey, setRangeKey] = useState("30d");
- const [breakdownMode, setBreakdownMode] = useState("project");
const range = useMemo(() => getDashboardRange(rangeKey), [rangeKey]);
const trackerOverview = useTrackerOverviewQuery(range);
const metrics = useMemo(
@@ -54,33 +56,10 @@ export default function TrackerDashboardPage() {
}),
[rangeKey, trackerOverview.data?.entries, trackerOverview.data?.tags],
);
- const breakdownItems = useMemo(() => {
- if (breakdownMode === "billability") {
- return [
- {
- name: "Billable",
- seconds: metrics.billableSeconds,
- hours: metrics.billableSeconds / 3600,
- percentage: metrics.billableShare,
- fill: "hsl(200 78% 46%)",
- },
- {
- name: "Non-billable",
- seconds: metrics.nonBillableSeconds,
- hours: metrics.nonBillableSeconds / 3600,
- percentage: metrics.nonBillableShare,
- fill: "hsl(210 16% 82%)",
- },
- ].filter((item) => item.seconds > 0);
- }
- return metrics.projects;
- }, [breakdownMode, metrics]);
- const breakdownTitle = breakdownMode === "project" ? "Project split" : "Billable split";
- const breakdownDescription =
- breakdownMode === "project"
- ? "Distribution of tracked time across projects"
- : "Distribution of tracked time across billable and non-billable work";
+ const breakdownItems = metrics.projects;
+ const breakdownTitle = "Project split";
+ const breakdownDescription = "Distribution of tracked time across projects";
const rangeLabel =
DASHBOARD_RANGE_OPTIONS.find((option) => option.key === rangeKey)?.label ?? "Last 30 days";
@@ -89,47 +68,38 @@ export default function TrackerDashboardPage() {
const hasEntries = metrics.totalSeconds > 0;
return (
-
-
-
-
Dashboard
-
- {activeEntry
- ? `Running now: ${getEntryDescriptionLabel(activeEntry.description)} - ${formatMetricDuration(activeTimerSeconds)}`
- : "Overview of tracked time across your recent work."}
-
-
-
- setBreakdownMode(value as BreakdownMode)}
- placeholder="Project"
- options={[
- { value: "project", label: "Project" },
- { value: "billability", label: "Billability" },
- ]}
- />
- undefined}
- placeholder="Owner"
- options={[{ value: "me", label: "Only me" }]}
- />
- setRangeKey(value as DashboardRangeKey)}
- placeholder="Range"
- options={DASHBOARD_RANGE_OPTIONS.map((option) => ({
- value: option.key,
- label: option.label,
- }))}
- icon={}
- />
-
-
+
+
+ setRangeKey(value as DashboardRangeKey)}
+ placeholder="Range"
+ options={DASHBOARD_RANGE_OPTIONS.map((option) => ({
+ value: option.key,
+ label: option.label,
+ }))}
+ icon={}
+ />
+ >
+ }
+ >
+
+
@@ -161,7 +131,7 @@ export default function TrackerDashboardPage() {
{Array.from({ length: 3 }, (_, index) => (
-
+
))}
@@ -186,8 +156,11 @@ export default function TrackerDashboardPage() {
- {Array.from({ length: 3 }, (_, i) => (
-
+ {Array.from({ length: 3 }, (_, index) => (
+
))}
@@ -202,7 +175,7 @@ export default function TrackerDashboardPage() {
) : (
)}
@@ -222,7 +195,7 @@ export default function TrackerDashboardPage() {
{trackerOverview.isLoading ? (
{Array.from({ length: 8 }, (_, index) => (
-
+
))}
) : metrics.activities.length ? (
@@ -249,14 +222,14 @@ export default function TrackerDashboardPage() {
) : (
)}
-
+
);
}
@@ -307,7 +280,7 @@ function SummaryCell({
{label}
diff --git a/apps/web/src/features/time-tracker/styles/react-big-calendar.css b/apps/web/src/features/time-tracker/styles/react-big-calendar.css
index ae6afe0..9d99a7c 100644
--- a/apps/web/src/features/time-tracker/styles/react-big-calendar.css
+++ b/apps/web/src/features/time-tracker/styles/react-big-calendar.css
@@ -1,5 +1,4 @@
.open-clock-calendar {
- --calendar-billable: var(--color-billable);
--calendar-tracked: var(--color-tracked);
--calendar-active: color-mix(in srgb, var(--foreground) 12%, transparent);
--calendar-grid: color-mix(in srgb, var(--border) 90%, transparent);
@@ -95,8 +94,8 @@
}
.open-clock-calendar .rbc-current-time-indicator {
- border-top: 1px solid var(--calendar-billable);
- background: var(--calendar-billable);
+ border-top: 1px solid var(--calendar-tracked);
+ background: var(--calendar-tracked);
height: 1px;
}
@@ -116,11 +115,6 @@
box-shadow: inset 0 0 0 1px var(--ring);
}
-.open-clock-calendar .rbc-event.calendar-entry-event--billable {
- background: color-mix(in srgb, var(--calendar-billable) 10%, var(--background));
- border-color: color-mix(in srgb, var(--calendar-billable) 35%, transparent);
-}
-
.open-clock-calendar .rbc-event.calendar-entry-event--tracked {
background: color-mix(in srgb, var(--calendar-tracked) 10%, var(--background));
border-color: color-mix(in srgb, var(--calendar-tracked) 35%, transparent);
diff --git a/apps/web/src/features/time-tracker/utils/calendar.ts b/apps/web/src/features/time-tracker/utils/calendar.ts
index 7a56e9c..61dab21 100644
--- a/apps/web/src/features/time-tracker/utils/calendar.ts
+++ b/apps/web/src/features/time-tracker/utils/calendar.ts
@@ -31,7 +31,6 @@ export interface CalendarEntryEventResource {
taskTitle: string | null;
taskDisplayKey: string | null;
tagNames: string[];
- isBillable: boolean;
isActive: boolean;
durationSeconds: number;
canEdit: boolean;
@@ -102,7 +101,6 @@ function toCalendarEvent(entry: TrackerEntry, options?: { isActive?: boolean; no
taskTitle: entry.task?.title ?? null,
taskDisplayKey: entry.task?.displayKey ?? null,
tagNames: entry.tags.map((tag) => tag.name),
- isBillable: entry.isBillable,
isActive,
durationSeconds: isActive
? getElapsedSeconds(entry.startAt, now)
@@ -198,9 +196,7 @@ export function getCalendarEventClassName(event: CalendarEntryEvent) {
return "calendar-entry-event calendar-entry-event--active";
}
- return event.resource.isBillable
- ? "calendar-entry-event calendar-entry-event--billable"
- : "calendar-entry-event calendar-entry-event--tracked";
+ return "calendar-entry-event calendar-entry-event--tracked";
}
export function formatCalendarEventDuration(totalSeconds: number) {
diff --git a/apps/web/src/features/time-tracker/utils/dashboard.ts b/apps/web/src/features/time-tracker/utils/dashboard.ts
index 8564903..f819c2d 100644
--- a/apps/web/src/features/time-tracker/utils/dashboard.ts
+++ b/apps/web/src/features/time-tracker/utils/dashboard.ts
@@ -54,7 +54,6 @@ export interface DashboardDailyDatum {
dateKey: string;
label: string;
totalHours: number;
- billableHours: number;
}
export interface DashboardProjectDatum {
@@ -75,10 +74,6 @@ export interface DashboardActivityDatum {
export interface TrackerDashboardMetrics {
totalSeconds: number;
- billableSeconds: number;
- nonBillableSeconds: number;
- billableShare: number;
- nonBillableShare: number;
averageDaySeconds: number;
trackedDays: number;
topProjectName: string;
@@ -190,7 +185,6 @@ export function buildTrackerDashboardMetrics({
dateKey,
label: formatAxisLabel(day),
totalHours: 0,
- billableHours: 0,
});
}
@@ -202,7 +196,6 @@ export function buildTrackerDashboardMetrics({
>();
let totalSeconds = 0;
- let billableSeconds = 0;
let totalSessions = 0;
for (const entry of entries) {
@@ -215,10 +208,6 @@ export function buildTrackerDashboardMetrics({
totalSessions += 1;
totalSeconds += durationSeconds;
- if (entry.isBillable) {
- billableSeconds += durationSeconds;
- }
-
const startDate = new Date(entry.startAt);
const dateKey = toDateKey(startDate);
const day = dailyMap.get(dateKey);
@@ -226,10 +215,6 @@ export function buildTrackerDashboardMetrics({
if (day) {
day.totalHours += durationHours;
-
- if (entry.isBillable) {
- day.billableHours += durationHours;
- }
}
const projectName = entry.project?.name ?? "Without project";
@@ -300,11 +285,6 @@ export function buildTrackerDashboardMetrics({
return {
totalSeconds,
- billableSeconds,
- nonBillableSeconds: totalSeconds - billableSeconds,
- billableShare: totalSeconds > 0 ? (billableSeconds / totalSeconds) * 100 : 0,
- nonBillableShare:
- totalSeconds > 0 ? ((totalSeconds - billableSeconds) / totalSeconds) * 100 : 0,
averageDaySeconds,
trackedDays,
topProjectName,
diff --git a/apps/web/src/features/time-tracker/utils/date-time.ts b/apps/web/src/features/time-tracker/utils/date-time.ts
index e0a739d..705b3bd 100644
--- a/apps/web/src/features/time-tracker/utils/date-time.ts
+++ b/apps/web/src/features/time-tracker/utils/date-time.ts
@@ -77,7 +77,6 @@ export function getDefaultManualValues() {
projectId: null as number | null,
taskId: null as number | null,
tagIds: [] as number[],
- isBillable: false,
};
}
@@ -87,7 +86,6 @@ export function getTimerFormValues(entry?: TrackerEntry | null) {
projectId: entry?.project?.id ?? null,
taskId: entry?.task?.id ?? null,
tagIds: entry?.tags.map((tag) => tag.id) ?? [],
- isBillable: entry?.isBillable ?? false,
};
}
@@ -103,7 +101,6 @@ export function getEditableEntryValues(entry: TrackerEntry) {
projectId: entry.project?.id ?? null,
taskId: entry.task?.id ?? null,
tagIds: entry.tags.map((tag) => tag.id),
- isBillable: entry.isBillable,
};
}
diff --git a/apps/web/src/features/time-tracker/utils/reports.ts b/apps/web/src/features/time-tracker/utils/reports.ts
index 3286faa..01e6794 100644
--- a/apps/web/src/features/time-tracker/utils/reports.ts
+++ b/apps/web/src/features/time-tracker/utils/reports.ts
@@ -33,7 +33,6 @@ export interface ReportDailyDatum {
dateKey: string;
label: string;
totalHours: number;
- billableHours: number;
}
export interface ReportProjectDatum {
@@ -52,14 +51,11 @@ export interface ReportTableRow {
hours: string;
projectName: string;
tagsLabel: string;
- billableLabel: string;
description: string;
}
export interface TrackerReportMetrics {
totalSeconds: number;
- billableSeconds: number;
- nonBillableSeconds: number;
totalSessions: number;
activeDays: number;
projectCount: number;
@@ -267,7 +263,6 @@ export function buildTrackerReportMetrics({
dateKey,
label: formatAxisLabel(day),
totalHours: 0,
- billableHours: 0,
});
}
@@ -286,13 +281,11 @@ export function buildTrackerReportMetrics({
hours: formatDecimalHours(durationSeconds),
projectName: entry.project?.name ?? "Without project",
tagsLabel: entry.tags.length ? entry.tags.map((tag) => tag.name).join(", ") : "-",
- billableLabel: entry.isBillable ? "Billable" : "Non-billable",
description: getEntryDescriptionLabel(entry.description),
} satisfies ReportTableRow;
});
let totalSeconds = 0;
- let billableSeconds = 0;
for (const entry of entries) {
const durationSeconds = getEntryDurationSeconds(entry);
@@ -303,19 +296,12 @@ export function buildTrackerReportMetrics({
totalSeconds += durationSeconds;
- if (entry.isBillable) {
- billableSeconds += durationSeconds;
- }
-
const startDate = new Date(entry.startAt);
const dateKey = toDateKey(startDate);
const day = dailyMap.get(dateKey);
if (day) {
day.totalHours += formatHours(durationSeconds);
- if (entry.isBillable) {
- day.billableHours += formatHours(durationSeconds);
- }
}
const projectName = entry.project?.name ?? "Without project";
@@ -345,8 +331,6 @@ export function buildTrackerReportMetrics({
return {
totalSeconds,
- billableSeconds,
- nonBillableSeconds: totalSeconds - billableSeconds,
totalSessions: rows.length,
activeDays: [...dailyMap.values()].filter((day) => day.totalHours > 0).length,
projectCount: projectTotals.size,
@@ -376,8 +360,6 @@ export function exportTrackerReport({
const summary = {
range: rangeLabel,
totalTracked: formatDuration(metrics.totalSeconds),
- billable: formatDuration(metrics.billableSeconds),
- nonBillable: formatDuration(metrics.nonBillableSeconds),
sessions: metrics.totalSessions,
activeDays: metrics.activeDays,
projects: metrics.projectCount,
@@ -396,13 +378,11 @@ export function exportTrackerReport({
const lines = [
["Range", summary.range],
["Total tracked", summary.totalTracked],
- ["Billable", summary.billable],
- ["Non-billable", summary.nonBillable],
["Sessions", String(summary.sessions)],
["Active days", String(summary.activeDays)],
["Projects", String(summary.projects)],
[],
- ["Date", "Start", "End", "Duration", "Hours", "Project", "Tags", "Billable", "Description"],
+ ["Date", "Start", "End", "Duration", "Hours", "Project", "Tags", "Description"],
...metrics.rows.map((row) => [
row.dateLabel,
row.startTime,
@@ -411,7 +391,6 @@ export function exportTrackerReport({
row.hours,
row.projectName,
row.tagsLabel,
- row.billableLabel,
row.description,
]),
];
@@ -435,7 +414,6 @@ export function exportTrackerReport({
${escapeHtml(row.hours)} |
${escapeHtml(row.projectName)} |
${escapeHtml(row.tagsLabel)} |
-
${escapeHtml(row.billableLabel)} |
${escapeHtml(row.description)} |
`,
)
@@ -458,8 +436,6 @@ export function exportTrackerReport({
| Time report summary |
| Range | ${escapeHtml(summary.range)} |
| Total tracked | ${escapeHtml(summary.totalTracked)} |
-
| Billable | ${escapeHtml(summary.billable)} |
-
| Non-billable | ${escapeHtml(summary.nonBillable)} |
| Sessions | ${summary.sessions} |
| Active days | ${summary.activeDays} |
| Projects | ${summary.projects} |
@@ -475,7 +451,6 @@ export function exportTrackerReport({
Hours |
Project |
Tags |
-
Billable |
Description |
diff --git a/apps/web/src/hooks/use-table-selection.ts b/apps/web/src/hooks/use-table-selection.ts
new file mode 100644
index 0000000..2fa0ba7
--- /dev/null
+++ b/apps/web/src/hooks/use-table-selection.ts
@@ -0,0 +1,57 @@
+import { useEffect, useState } from "react";
+
+interface UseTableSelectionOptions
{
+ items: T[];
+}
+
+interface UseTableSelectionReturn {
+ selectedIds: Set;
+ toggleSelect: (id: number) => void;
+ toggleSelectAll: () => void;
+ allSelected: boolean;
+ someSelected: boolean;
+}
+
+export function useTableSelection({
+ items,
+}: UseTableSelectionOptions): UseTableSelectionReturn {
+ const [selectedIds, setSelectedIds] = useState>(new Set());
+
+ useEffect(() => {
+ const visibleIds = new Set(items.map((item) => item.id));
+ setSelectedIds((prev) => {
+ const next = new Set([...prev].filter((id) => visibleIds.has(id)));
+ return next.size === prev.size ? prev : next;
+ });
+ }, [items]);
+
+ function toggleSelect(id: number) {
+ setSelectedIds((prev) => {
+ const next = new Set(prev);
+ next.has(id) ? next.delete(id) : next.add(id);
+ return next;
+ });
+ }
+
+ function toggleSelectAll() {
+ setSelectedIds((prev) => {
+ const next = new Set(prev);
+ const areAllSelected = items.length > 0 && items.every((item) => prev.has(item.id));
+ if (areAllSelected) {
+ for (const item of items) {
+ next.delete(item.id);
+ }
+ } else {
+ for (const item of items) {
+ next.add(item.id);
+ }
+ }
+ return next;
+ });
+ }
+
+ const allSelected = items.length > 0 && items.every((item) => selectedIds.has(item.id));
+ const someSelected = items.some((item) => selectedIds.has(item.id)) && !allSelected;
+
+ return { selectedIds, toggleSelect, toggleSelectAll, allSelected, someSelected };
+}
diff --git a/apps/web/src/public/hero-image-dark.png b/apps/web/src/public/hero-image-dark.png
new file mode 100644
index 0000000..ec3c1f1
Binary files /dev/null and b/apps/web/src/public/hero-image-dark.png differ
diff --git a/apps/web/src/public/hero-image-light.png b/apps/web/src/public/hero-image-light.png
new file mode 100644
index 0000000..527fc8f
Binary files /dev/null and b/apps/web/src/public/hero-image-light.png differ
diff --git a/apps/web/src/public/logo.svg b/apps/web/src/public/logo.svg
new file mode 100644
index 0000000..d2d4782
--- /dev/null
+++ b/apps/web/src/public/logo.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 693ac27..3d3e00e 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -8,339 +8,512 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
-import { Route as rootRouteImport } from './routes/__root'
-import { Route as SuccessRouteImport } from './routes/success'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as AppAppRouteImport } from './routes/app._app'
-import { Route as AcceptInvitationInvitationIdRouteImport } from './routes/accept-invitation.$invitationId'
-import { Route as AppAppIndexRouteImport } from './routes/app._app.index'
-import { Route as AppAppTrackerRouteImport } from './routes/app._app.tracker'
-import { Route as AppAppTeamsRouteImport } from './routes/app._app.teams'
-import { Route as AppAppTasksRouteImport } from './routes/app._app.tasks'
-import { Route as AppAppTagsRouteImport } from './routes/app._app.tags'
-import { Route as AppAppReportsRouteImport } from './routes/app._app.reports'
-import { Route as AppAppProjectsRouteImport } from './routes/app._app.projects'
-import { Route as AppAppClientsRouteImport } from './routes/app._app.clients'
-import { Route as AppAppCalendarRouteImport } from './routes/app._app.calendar'
-import { Route as AppAppAiRouteImport } from './routes/app._app.ai'
+import { Route as rootRouteImport } from "./routes/__root";
+import { Route as SuccessRouteImport } from "./routes/success";
+import { Route as LoginRouteImport } from "./routes/login";
+import { Route as IndexRouteImport } from "./routes/index";
+import { Route as AppAppRouteImport } from "./routes/app._app";
+import { Route as AcceptInvitationInvitationIdRouteImport } from "./routes/accept-invitation.$invitationId";
+import { Route as AppAppIndexRouteImport } from "./routes/app._app.index";
+import { Route as AppAppTrackerRouteImport } from "./routes/app._app.tracker";
+import { Route as AppAppTeamsRouteImport } from "./routes/app._app.teams";
+import { Route as AppAppTasksRouteImport } from "./routes/app._app.tasks";
+import { Route as AppAppTagsRouteImport } from "./routes/app._app.tags";
+import { Route as AppAppReportsRouteImport } from "./routes/app._app.reports";
+import { Route as AppAppProjectsRouteImport } from "./routes/app._app.projects";
+import { Route as AppAppOverviewRouteImport } from "./routes/app._app.overview";
+import { Route as AppAppManageRouteImport } from "./routes/app._app.manage";
+import { Route as AppAppDashboardRouteImport } from "./routes/app._app.dashboard";
+import { Route as AppAppClientsRouteImport } from "./routes/app._app.clients";
+import { Route as AppAppCalendarRouteImport } from "./routes/app._app.calendar";
+import { Route as AppAppAiRouteImport } from "./routes/app._app.ai";
+import { Route as AppAppManageIndexRouteImport } from "./routes/app._app.manage.index";
+import { Route as AppAppManageTeamsRouteImport } from "./routes/app._app.manage.teams";
+import { Route as AppAppManageTagsRouteImport } from "./routes/app._app.manage.tags";
+import { Route as AppAppManageProjectsRouteImport } from "./routes/app._app.manage.projects";
+import { Route as AppAppManageClientsRouteImport } from "./routes/app._app.manage.clients";
const SuccessRoute = SuccessRouteImport.update({
- id: '/success',
- path: '/success',
+ id: "/success",
+ path: "/success",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const LoginRoute = LoginRouteImport.update({
- id: '/login',
- path: '/login',
+ id: "/login",
+ path: "/login",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+ id: "/",
+ path: "/",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const AppAppRoute = AppAppRouteImport.update({
- id: '/app/_app',
- path: '/app',
+ id: "/app/_app",
+ path: "/app",
getParentRoute: () => rootRouteImport,
-} as any)
-const AcceptInvitationInvitationIdRoute =
- AcceptInvitationInvitationIdRouteImport.update({
- id: '/accept-invitation/$invitationId',
- path: '/accept-invitation/$invitationId',
- getParentRoute: () => rootRouteImport,
- } as any)
+} as any);
+const AcceptInvitationInvitationIdRoute = AcceptInvitationInvitationIdRouteImport.update({
+ id: "/accept-invitation/$invitationId",
+ path: "/accept-invitation/$invitationId",
+ getParentRoute: () => rootRouteImport,
+} as any);
const AppAppIndexRoute = AppAppIndexRouteImport.update({
- id: '/',
- path: '/',
+ id: "/",
+ path: "/",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppTrackerRoute = AppAppTrackerRouteImport.update({
- id: '/tracker',
- path: '/tracker',
+ id: "/tracker",
+ path: "/tracker",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppTeamsRoute = AppAppTeamsRouteImport.update({
- id: '/teams',
- path: '/teams',
+ id: "/teams",
+ path: "/teams",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppTasksRoute = AppAppTasksRouteImport.update({
- id: '/tasks',
- path: '/tasks',
+ id: "/tasks",
+ path: "/tasks",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppTagsRoute = AppAppTagsRouteImport.update({
- id: '/tags',
- path: '/tags',
+ id: "/tags",
+ path: "/tags",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppReportsRoute = AppAppReportsRouteImport.update({
- id: '/reports',
- path: '/reports',
+ id: "/reports",
+ path: "/reports",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppProjectsRoute = AppAppProjectsRouteImport.update({
- id: '/projects',
- path: '/projects',
+ id: "/projects",
+ path: "/projects",
+ getParentRoute: () => AppAppRoute,
+} as any);
+const AppAppOverviewRoute = AppAppOverviewRouteImport.update({
+ id: "/overview",
+ path: "/overview",
+ getParentRoute: () => AppAppRoute,
+} as any);
+const AppAppManageRoute = AppAppManageRouteImport.update({
+ id: "/manage",
+ path: "/manage",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
+const AppAppDashboardRoute = AppAppDashboardRouteImport.update({
+ id: "/dashboard",
+ path: "/dashboard",
+ getParentRoute: () => AppAppRoute,
+} as any);
const AppAppClientsRoute = AppAppClientsRouteImport.update({
- id: '/clients',
- path: '/clients',
+ id: "/clients",
+ path: "/clients",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppCalendarRoute = AppAppCalendarRouteImport.update({
- id: '/calendar',
- path: '/calendar',
+ id: "/calendar",
+ path: "/calendar",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
const AppAppAiRoute = AppAppAiRouteImport.update({
- id: '/ai',
- path: '/ai',
+ id: "/ai",
+ path: "/ai",
getParentRoute: () => AppAppRoute,
-} as any)
+} as any);
+const AppAppManageIndexRoute = AppAppManageIndexRouteImport.update({
+ id: "/",
+ path: "/",
+ getParentRoute: () => AppAppManageRoute,
+} as any);
+const AppAppManageTeamsRoute = AppAppManageTeamsRouteImport.update({
+ id: "/teams",
+ path: "/teams",
+ getParentRoute: () => AppAppManageRoute,
+} as any);
+const AppAppManageTagsRoute = AppAppManageTagsRouteImport.update({
+ id: "/tags",
+ path: "/tags",
+ getParentRoute: () => AppAppManageRoute,
+} as any);
+const AppAppManageProjectsRoute = AppAppManageProjectsRouteImport.update({
+ id: "/projects",
+ path: "/projects",
+ getParentRoute: () => AppAppManageRoute,
+} as any);
+const AppAppManageClientsRoute = AppAppManageClientsRouteImport.update({
+ id: "/clients",
+ path: "/clients",
+ getParentRoute: () => AppAppManageRoute,
+} as any);
export interface FileRoutesByFullPath {
- '/': typeof IndexRoute
- '/login': typeof LoginRoute
- '/success': typeof SuccessRoute
- '/accept-invitation/$invitationId': typeof AcceptInvitationInvitationIdRoute
- '/app': typeof AppAppRouteWithChildren
- '/app/ai': typeof AppAppAiRoute
- '/app/calendar': typeof AppAppCalendarRoute
- '/app/clients': typeof AppAppClientsRoute
- '/app/projects': typeof AppAppProjectsRoute
- '/app/reports': typeof AppAppReportsRoute
- '/app/tags': typeof AppAppTagsRoute
- '/app/tasks': typeof AppAppTasksRoute
- '/app/teams': typeof AppAppTeamsRoute
- '/app/tracker': typeof AppAppTrackerRoute
- '/app/': typeof AppAppIndexRoute
+ "/": typeof IndexRoute;
+ "/login": typeof LoginRoute;
+ "/success": typeof SuccessRoute;
+ "/accept-invitation/$invitationId": typeof AcceptInvitationInvitationIdRoute;
+ "/app": typeof AppAppRouteWithChildren;
+ "/app/ai": typeof AppAppAiRoute;
+ "/app/calendar": typeof AppAppCalendarRoute;
+ "/app/clients": typeof AppAppClientsRoute;
+ "/app/dashboard": typeof AppAppDashboardRoute;
+ "/app/manage": typeof AppAppManageRouteWithChildren;
+ "/app/overview": typeof AppAppOverviewRoute;
+ "/app/projects": typeof AppAppProjectsRoute;
+ "/app/reports": typeof AppAppReportsRoute;
+ "/app/tags": typeof AppAppTagsRoute;
+ "/app/tasks": typeof AppAppTasksRoute;
+ "/app/teams": typeof AppAppTeamsRoute;
+ "/app/tracker": typeof AppAppTrackerRoute;
+ "/app/": typeof AppAppIndexRoute;
+ "/app/manage/clients": typeof AppAppManageClientsRoute;
+ "/app/manage/projects": typeof AppAppManageProjectsRoute;
+ "/app/manage/tags": typeof AppAppManageTagsRoute;
+ "/app/manage/teams": typeof AppAppManageTeamsRoute;
+ "/app/manage/": typeof AppAppManageIndexRoute;
}
export interface FileRoutesByTo {
- '/': typeof IndexRoute
- '/login': typeof LoginRoute
- '/success': typeof SuccessRoute
- '/accept-invitation/$invitationId': typeof AcceptInvitationInvitationIdRoute
- '/app/ai': typeof AppAppAiRoute
- '/app/calendar': typeof AppAppCalendarRoute
- '/app/clients': typeof AppAppClientsRoute
- '/app/projects': typeof AppAppProjectsRoute
- '/app/reports': typeof AppAppReportsRoute
- '/app/tags': typeof AppAppTagsRoute
- '/app/tasks': typeof AppAppTasksRoute
- '/app/teams': typeof AppAppTeamsRoute
- '/app/tracker': typeof AppAppTrackerRoute
- '/app': typeof AppAppIndexRoute
+ "/": typeof IndexRoute;
+ "/login": typeof LoginRoute;
+ "/success": typeof SuccessRoute;
+ "/accept-invitation/$invitationId": typeof AcceptInvitationInvitationIdRoute;
+ "/app/ai": typeof AppAppAiRoute;
+ "/app/calendar": typeof AppAppCalendarRoute;
+ "/app/clients": typeof AppAppClientsRoute;
+ "/app/dashboard": typeof AppAppDashboardRoute;
+ "/app/overview": typeof AppAppOverviewRoute;
+ "/app/projects": typeof AppAppProjectsRoute;
+ "/app/reports": typeof AppAppReportsRoute;
+ "/app/tags": typeof AppAppTagsRoute;
+ "/app/tasks": typeof AppAppTasksRoute;
+ "/app/teams": typeof AppAppTeamsRoute;
+ "/app/tracker": typeof AppAppTrackerRoute;
+ "/app": typeof AppAppIndexRoute;
+ "/app/manage/clients": typeof AppAppManageClientsRoute;
+ "/app/manage/projects": typeof AppAppManageProjectsRoute;
+ "/app/manage/tags": typeof AppAppManageTagsRoute;
+ "/app/manage/teams": typeof AppAppManageTeamsRoute;
+ "/app/manage": typeof AppAppManageIndexRoute;
}
export interface FileRoutesById {
- __root__: typeof rootRouteImport
- '/': typeof IndexRoute
- '/login': typeof LoginRoute
- '/success': typeof SuccessRoute
- '/accept-invitation/$invitationId': typeof AcceptInvitationInvitationIdRoute
- '/app/_app': typeof AppAppRouteWithChildren
- '/app/_app/ai': typeof AppAppAiRoute
- '/app/_app/calendar': typeof AppAppCalendarRoute
- '/app/_app/clients': typeof AppAppClientsRoute
- '/app/_app/projects': typeof AppAppProjectsRoute
- '/app/_app/reports': typeof AppAppReportsRoute
- '/app/_app/tags': typeof AppAppTagsRoute
- '/app/_app/tasks': typeof AppAppTasksRoute
- '/app/_app/teams': typeof AppAppTeamsRoute
- '/app/_app/tracker': typeof AppAppTrackerRoute
- '/app/_app/': typeof AppAppIndexRoute
+ __root__: typeof rootRouteImport;
+ "/": typeof IndexRoute;
+ "/login": typeof LoginRoute;
+ "/success": typeof SuccessRoute;
+ "/accept-invitation/$invitationId": typeof AcceptInvitationInvitationIdRoute;
+ "/app/_app": typeof AppAppRouteWithChildren;
+ "/app/_app/ai": typeof AppAppAiRoute;
+ "/app/_app/calendar": typeof AppAppCalendarRoute;
+ "/app/_app/clients": typeof AppAppClientsRoute;
+ "/app/_app/dashboard": typeof AppAppDashboardRoute;
+ "/app/_app/manage": typeof AppAppManageRouteWithChildren;
+ "/app/_app/overview": typeof AppAppOverviewRoute;
+ "/app/_app/projects": typeof AppAppProjectsRoute;
+ "/app/_app/reports": typeof AppAppReportsRoute;
+ "/app/_app/tags": typeof AppAppTagsRoute;
+ "/app/_app/tasks": typeof AppAppTasksRoute;
+ "/app/_app/teams": typeof AppAppTeamsRoute;
+ "/app/_app/tracker": typeof AppAppTrackerRoute;
+ "/app/_app/": typeof AppAppIndexRoute;
+ "/app/_app/manage/clients": typeof AppAppManageClientsRoute;
+ "/app/_app/manage/projects": typeof AppAppManageProjectsRoute;
+ "/app/_app/manage/tags": typeof AppAppManageTagsRoute;
+ "/app/_app/manage/teams": typeof AppAppManageTeamsRoute;
+ "/app/_app/manage/": typeof AppAppManageIndexRoute;
}
export interface FileRouteTypes {
- fileRoutesByFullPath: FileRoutesByFullPath
+ fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
- | '/'
- | '/login'
- | '/success'
- | '/accept-invitation/$invitationId'
- | '/app'
- | '/app/ai'
- | '/app/calendar'
- | '/app/clients'
- | '/app/projects'
- | '/app/reports'
- | '/app/tags'
- | '/app/tasks'
- | '/app/teams'
- | '/app/tracker'
- | '/app/'
- fileRoutesByTo: FileRoutesByTo
+ | "/"
+ | "/login"
+ | "/success"
+ | "/accept-invitation/$invitationId"
+ | "/app"
+ | "/app/ai"
+ | "/app/calendar"
+ | "/app/clients"
+ | "/app/dashboard"
+ | "/app/manage"
+ | "/app/overview"
+ | "/app/projects"
+ | "/app/reports"
+ | "/app/tags"
+ | "/app/tasks"
+ | "/app/teams"
+ | "/app/tracker"
+ | "/app/"
+ | "/app/manage/clients"
+ | "/app/manage/projects"
+ | "/app/manage/tags"
+ | "/app/manage/teams"
+ | "/app/manage/";
+ fileRoutesByTo: FileRoutesByTo;
to:
- | '/'
- | '/login'
- | '/success'
- | '/accept-invitation/$invitationId'
- | '/app/ai'
- | '/app/calendar'
- | '/app/clients'
- | '/app/projects'
- | '/app/reports'
- | '/app/tags'
- | '/app/tasks'
- | '/app/teams'
- | '/app/tracker'
- | '/app'
+ | "/"
+ | "/login"
+ | "/success"
+ | "/accept-invitation/$invitationId"
+ | "/app/ai"
+ | "/app/calendar"
+ | "/app/clients"
+ | "/app/dashboard"
+ | "/app/overview"
+ | "/app/projects"
+ | "/app/reports"
+ | "/app/tags"
+ | "/app/tasks"
+ | "/app/teams"
+ | "/app/tracker"
+ | "/app"
+ | "/app/manage/clients"
+ | "/app/manage/projects"
+ | "/app/manage/tags"
+ | "/app/manage/teams"
+ | "/app/manage";
id:
- | '__root__'
- | '/'
- | '/login'
- | '/success'
- | '/accept-invitation/$invitationId'
- | '/app/_app'
- | '/app/_app/ai'
- | '/app/_app/calendar'
- | '/app/_app/clients'
- | '/app/_app/projects'
- | '/app/_app/reports'
- | '/app/_app/tags'
- | '/app/_app/tasks'
- | '/app/_app/teams'
- | '/app/_app/tracker'
- | '/app/_app/'
- fileRoutesById: FileRoutesById
+ | "__root__"
+ | "/"
+ | "/login"
+ | "/success"
+ | "/accept-invitation/$invitationId"
+ | "/app/_app"
+ | "/app/_app/ai"
+ | "/app/_app/calendar"
+ | "/app/_app/clients"
+ | "/app/_app/dashboard"
+ | "/app/_app/manage"
+ | "/app/_app/overview"
+ | "/app/_app/projects"
+ | "/app/_app/reports"
+ | "/app/_app/tags"
+ | "/app/_app/tasks"
+ | "/app/_app/teams"
+ | "/app/_app/tracker"
+ | "/app/_app/"
+ | "/app/_app/manage/clients"
+ | "/app/_app/manage/projects"
+ | "/app/_app/manage/tags"
+ | "/app/_app/manage/teams"
+ | "/app/_app/manage/";
+ fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
- IndexRoute: typeof IndexRoute
- LoginRoute: typeof LoginRoute
- SuccessRoute: typeof SuccessRoute
- AcceptInvitationInvitationIdRoute: typeof AcceptInvitationInvitationIdRoute
- AppAppRoute: typeof AppAppRouteWithChildren
+ IndexRoute: typeof IndexRoute;
+ LoginRoute: typeof LoginRoute;
+ SuccessRoute: typeof SuccessRoute;
+ AcceptInvitationInvitationIdRoute: typeof AcceptInvitationInvitationIdRoute;
+ AppAppRoute: typeof AppAppRouteWithChildren;
}
-declare module '@tanstack/react-router' {
+declare module "@tanstack/react-router" {
interface FileRoutesByPath {
- '/success': {
- id: '/success'
- path: '/success'
- fullPath: '/success'
- preLoaderRoute: typeof SuccessRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/login': {
- id: '/login'
- path: '/login'
- fullPath: '/login'
- preLoaderRoute: typeof LoginRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/app/_app': {
- id: '/app/_app'
- path: '/app'
- fullPath: '/app'
- preLoaderRoute: typeof AppAppRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/accept-invitation/$invitationId': {
- id: '/accept-invitation/$invitationId'
- path: '/accept-invitation/$invitationId'
- fullPath: '/accept-invitation/$invitationId'
- preLoaderRoute: typeof AcceptInvitationInvitationIdRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/app/_app/': {
- id: '/app/_app/'
- path: '/'
- fullPath: '/app/'
- preLoaderRoute: typeof AppAppIndexRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/tracker': {
- id: '/app/_app/tracker'
- path: '/tracker'
- fullPath: '/app/tracker'
- preLoaderRoute: typeof AppAppTrackerRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/teams': {
- id: '/app/_app/teams'
- path: '/teams'
- fullPath: '/app/teams'
- preLoaderRoute: typeof AppAppTeamsRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/tasks': {
- id: '/app/_app/tasks'
- path: '/tasks'
- fullPath: '/app/tasks'
- preLoaderRoute: typeof AppAppTasksRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/tags': {
- id: '/app/_app/tags'
- path: '/tags'
- fullPath: '/app/tags'
- preLoaderRoute: typeof AppAppTagsRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/reports': {
- id: '/app/_app/reports'
- path: '/reports'
- fullPath: '/app/reports'
- preLoaderRoute: typeof AppAppReportsRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/projects': {
- id: '/app/_app/projects'
- path: '/projects'
- fullPath: '/app/projects'
- preLoaderRoute: typeof AppAppProjectsRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/clients': {
- id: '/app/_app/clients'
- path: '/clients'
- fullPath: '/app/clients'
- preLoaderRoute: typeof AppAppClientsRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/calendar': {
- id: '/app/_app/calendar'
- path: '/calendar'
- fullPath: '/app/calendar'
- preLoaderRoute: typeof AppAppCalendarRouteImport
- parentRoute: typeof AppAppRoute
- }
- '/app/_app/ai': {
- id: '/app/_app/ai'
- path: '/ai'
- fullPath: '/app/ai'
- preLoaderRoute: typeof AppAppAiRouteImport
- parentRoute: typeof AppAppRoute
- }
+ "/success": {
+ id: "/success";
+ path: "/success";
+ fullPath: "/success";
+ preLoaderRoute: typeof SuccessRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/login": {
+ id: "/login";
+ path: "/login";
+ fullPath: "/login";
+ preLoaderRoute: typeof LoginRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/": {
+ id: "/";
+ path: "/";
+ fullPath: "/";
+ preLoaderRoute: typeof IndexRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/app/_app": {
+ id: "/app/_app";
+ path: "/app";
+ fullPath: "/app";
+ preLoaderRoute: typeof AppAppRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/accept-invitation/$invitationId": {
+ id: "/accept-invitation/$invitationId";
+ path: "/accept-invitation/$invitationId";
+ fullPath: "/accept-invitation/$invitationId";
+ preLoaderRoute: typeof AcceptInvitationInvitationIdRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/app/_app/": {
+ id: "/app/_app/";
+ path: "/";
+ fullPath: "/app/";
+ preLoaderRoute: typeof AppAppIndexRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/tracker": {
+ id: "/app/_app/tracker";
+ path: "/tracker";
+ fullPath: "/app/tracker";
+ preLoaderRoute: typeof AppAppTrackerRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/teams": {
+ id: "/app/_app/teams";
+ path: "/teams";
+ fullPath: "/app/teams";
+ preLoaderRoute: typeof AppAppTeamsRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/tasks": {
+ id: "/app/_app/tasks";
+ path: "/tasks";
+ fullPath: "/app/tasks";
+ preLoaderRoute: typeof AppAppTasksRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/tags": {
+ id: "/app/_app/tags";
+ path: "/tags";
+ fullPath: "/app/tags";
+ preLoaderRoute: typeof AppAppTagsRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/reports": {
+ id: "/app/_app/reports";
+ path: "/reports";
+ fullPath: "/app/reports";
+ preLoaderRoute: typeof AppAppReportsRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/projects": {
+ id: "/app/_app/projects";
+ path: "/projects";
+ fullPath: "/app/projects";
+ preLoaderRoute: typeof AppAppProjectsRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/overview": {
+ id: "/app/_app/overview";
+ path: "/overview";
+ fullPath: "/app/overview";
+ preLoaderRoute: typeof AppAppOverviewRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/manage": {
+ id: "/app/_app/manage";
+ path: "/manage";
+ fullPath: "/app/manage";
+ preLoaderRoute: typeof AppAppManageRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/dashboard": {
+ id: "/app/_app/dashboard";
+ path: "/dashboard";
+ fullPath: "/app/dashboard";
+ preLoaderRoute: typeof AppAppDashboardRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/clients": {
+ id: "/app/_app/clients";
+ path: "/clients";
+ fullPath: "/app/clients";
+ preLoaderRoute: typeof AppAppClientsRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/calendar": {
+ id: "/app/_app/calendar";
+ path: "/calendar";
+ fullPath: "/app/calendar";
+ preLoaderRoute: typeof AppAppCalendarRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/ai": {
+ id: "/app/_app/ai";
+ path: "/ai";
+ fullPath: "/app/ai";
+ preLoaderRoute: typeof AppAppAiRouteImport;
+ parentRoute: typeof AppAppRoute;
+ };
+ "/app/_app/manage/": {
+ id: "/app/_app/manage/";
+ path: "/";
+ fullPath: "/app/manage/";
+ preLoaderRoute: typeof AppAppManageIndexRouteImport;
+ parentRoute: typeof AppAppManageRoute;
+ };
+ "/app/_app/manage/teams": {
+ id: "/app/_app/manage/teams";
+ path: "/teams";
+ fullPath: "/app/manage/teams";
+ preLoaderRoute: typeof AppAppManageTeamsRouteImport;
+ parentRoute: typeof AppAppManageRoute;
+ };
+ "/app/_app/manage/tags": {
+ id: "/app/_app/manage/tags";
+ path: "/tags";
+ fullPath: "/app/manage/tags";
+ preLoaderRoute: typeof AppAppManageTagsRouteImport;
+ parentRoute: typeof AppAppManageRoute;
+ };
+ "/app/_app/manage/projects": {
+ id: "/app/_app/manage/projects";
+ path: "/projects";
+ fullPath: "/app/manage/projects";
+ preLoaderRoute: typeof AppAppManageProjectsRouteImport;
+ parentRoute: typeof AppAppManageRoute;
+ };
+ "/app/_app/manage/clients": {
+ id: "/app/_app/manage/clients";
+ path: "/clients";
+ fullPath: "/app/manage/clients";
+ preLoaderRoute: typeof AppAppManageClientsRouteImport;
+ parentRoute: typeof AppAppManageRoute;
+ };
}
}
+interface AppAppManageRouteChildren {
+ AppAppManageClientsRoute: typeof AppAppManageClientsRoute;
+ AppAppManageProjectsRoute: typeof AppAppManageProjectsRoute;
+ AppAppManageTagsRoute: typeof AppAppManageTagsRoute;
+ AppAppManageTeamsRoute: typeof AppAppManageTeamsRoute;
+ AppAppManageIndexRoute: typeof AppAppManageIndexRoute;
+}
+
+const AppAppManageRouteChildren: AppAppManageRouteChildren = {
+ AppAppManageClientsRoute: AppAppManageClientsRoute,
+ AppAppManageProjectsRoute: AppAppManageProjectsRoute,
+ AppAppManageTagsRoute: AppAppManageTagsRoute,
+ AppAppManageTeamsRoute: AppAppManageTeamsRoute,
+ AppAppManageIndexRoute: AppAppManageIndexRoute,
+};
+
+const AppAppManageRouteWithChildren = AppAppManageRoute._addFileChildren(AppAppManageRouteChildren);
+
interface AppAppRouteChildren {
- AppAppAiRoute: typeof AppAppAiRoute
- AppAppCalendarRoute: typeof AppAppCalendarRoute
- AppAppClientsRoute: typeof AppAppClientsRoute
- AppAppProjectsRoute: typeof AppAppProjectsRoute
- AppAppReportsRoute: typeof AppAppReportsRoute
- AppAppTagsRoute: typeof AppAppTagsRoute
- AppAppTasksRoute: typeof AppAppTasksRoute
- AppAppTeamsRoute: typeof AppAppTeamsRoute
- AppAppTrackerRoute: typeof AppAppTrackerRoute
- AppAppIndexRoute: typeof AppAppIndexRoute
+ AppAppAiRoute: typeof AppAppAiRoute;
+ AppAppCalendarRoute: typeof AppAppCalendarRoute;
+ AppAppClientsRoute: typeof AppAppClientsRoute;
+ AppAppDashboardRoute: typeof AppAppDashboardRoute;
+ AppAppManageRoute: typeof AppAppManageRouteWithChildren;
+ AppAppOverviewRoute: typeof AppAppOverviewRoute;
+ AppAppProjectsRoute: typeof AppAppProjectsRoute;
+ AppAppReportsRoute: typeof AppAppReportsRoute;
+ AppAppTagsRoute: typeof AppAppTagsRoute;
+ AppAppTasksRoute: typeof AppAppTasksRoute;
+ AppAppTeamsRoute: typeof AppAppTeamsRoute;
+ AppAppTrackerRoute: typeof AppAppTrackerRoute;
+ AppAppIndexRoute: typeof AppAppIndexRoute;
}
const AppAppRouteChildren: AppAppRouteChildren = {
AppAppAiRoute: AppAppAiRoute,
AppAppCalendarRoute: AppAppCalendarRoute,
AppAppClientsRoute: AppAppClientsRoute,
+ AppAppDashboardRoute: AppAppDashboardRoute,
+ AppAppManageRoute: AppAppManageRouteWithChildren,
+ AppAppOverviewRoute: AppAppOverviewRoute,
AppAppProjectsRoute: AppAppProjectsRoute,
AppAppReportsRoute: AppAppReportsRoute,
AppAppTagsRoute: AppAppTagsRoute,
@@ -348,10 +521,9 @@ const AppAppRouteChildren: AppAppRouteChildren = {
AppAppTeamsRoute: AppAppTeamsRoute,
AppAppTrackerRoute: AppAppTrackerRoute,
AppAppIndexRoute: AppAppIndexRoute,
-}
+};
-const AppAppRouteWithChildren =
- AppAppRoute._addFileChildren(AppAppRouteChildren)
+const AppAppRouteWithChildren = AppAppRoute._addFileChildren(AppAppRouteChildren);
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
@@ -359,7 +531,7 @@ const rootRouteChildren: RootRouteChildren = {
SuccessRoute: SuccessRoute,
AcceptInvitationInvitationIdRoute: AcceptInvitationInvitationIdRoute,
AppAppRoute: AppAppRouteWithChildren,
-}
+};
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
- ._addFileTypes()
+ ._addFileTypes();
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index 6e97b33..dd67e3f 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -19,11 +19,11 @@ export const Route = createRootRouteWithContext()({
head: () => ({
meta: [
{
- title: "Open Clock",
+ title: "better clock",
},
{
name: "description",
- content: "Open Clock is a web application",
+ content: "better clock is a web application",
},
],
links: [
diff --git a/apps/web/src/routes/app._app.clients.tsx b/apps/web/src/routes/app._app.clients.tsx
index f1fe351..725c9ce 100644
--- a/apps/web/src/routes/app._app.clients.tsx
+++ b/apps/web/src/routes/app._app.clients.tsx
@@ -1,6 +1,7 @@
-import { createFileRoute } from "@tanstack/react-router";
-import ClientsPage from "@/features/clients/pages/clients-page";
+import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/app/_app/clients")({
- component: ClientsPage,
+ beforeLoad: () => {
+ redirect({ to: "/app/manage/clients", throw: true });
+ },
});
diff --git a/apps/web/src/routes/app._app.dashboard.tsx b/apps/web/src/routes/app._app.dashboard.tsx
new file mode 100644
index 0000000..30d99ef
--- /dev/null
+++ b/apps/web/src/routes/app._app.dashboard.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute, redirect } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/app/_app/dashboard")({
+ beforeLoad: () => {
+ redirect({ to: "/app/overview", throw: true });
+ },
+});
diff --git a/apps/web/src/routes/app._app.index.tsx b/apps/web/src/routes/app._app.index.tsx
index e7effc1..77dfebf 100644
--- a/apps/web/src/routes/app._app.index.tsx
+++ b/apps/web/src/routes/app._app.index.tsx
@@ -1,7 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
-import TrackerDashboardPage from "@/features/time-tracker/pages/tracker-dashboard-page";
+import TimeTrackerPage from "@/features/time-tracker/pages/time-tracker-page";
export const Route = createFileRoute("/app/_app/")({
- component: TrackerDashboardPage,
+ component: TimeTrackerPage,
});
diff --git a/apps/web/src/routes/app._app.manage.clients.tsx b/apps/web/src/routes/app._app.manage.clients.tsx
new file mode 100644
index 0000000..c67c9ff
--- /dev/null
+++ b/apps/web/src/routes/app._app.manage.clients.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import ClientsPage from "@/features/clients/pages/clients-page";
+
+export const Route = createFileRoute("/app/_app/manage/clients")({
+ component: ClientsPage,
+});
diff --git a/apps/web/src/routes/app._app.manage.index.tsx b/apps/web/src/routes/app._app.manage.index.tsx
new file mode 100644
index 0000000..ae359c8
--- /dev/null
+++ b/apps/web/src/routes/app._app.manage.index.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute, redirect } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/app/_app/manage/")({
+ beforeLoad: () => {
+ redirect({ to: "/app/manage/projects", throw: true });
+ },
+});
diff --git a/apps/web/src/routes/app._app.manage.projects.tsx b/apps/web/src/routes/app._app.manage.projects.tsx
new file mode 100644
index 0000000..72ee226
--- /dev/null
+++ b/apps/web/src/routes/app._app.manage.projects.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import ProjectsPage from "@/features/projects/pages/projects-page";
+
+export const Route = createFileRoute("/app/_app/manage/projects")({
+ component: ProjectsPage,
+});
diff --git a/apps/web/src/routes/app._app.manage.tags.tsx b/apps/web/src/routes/app._app.manage.tags.tsx
new file mode 100644
index 0000000..89d5525
--- /dev/null
+++ b/apps/web/src/routes/app._app.manage.tags.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import TagsPage from "@/features/tags/pages/tags-page";
+
+export const Route = createFileRoute("/app/_app/manage/tags")({
+ component: TagsPage,
+});
diff --git a/apps/web/src/routes/app._app.manage.teams.tsx b/apps/web/src/routes/app._app.manage.teams.tsx
new file mode 100644
index 0000000..041e62f
--- /dev/null
+++ b/apps/web/src/routes/app._app.manage.teams.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import TeamsPage from "@/features/organization/pages/teams-page";
+
+export const Route = createFileRoute("/app/_app/manage/teams")({
+ component: TeamsPage,
+});
diff --git a/apps/web/src/routes/app._app.manage.tsx b/apps/web/src/routes/app._app.manage.tsx
new file mode 100644
index 0000000..130ea51
--- /dev/null
+++ b/apps/web/src/routes/app._app.manage.tsx
@@ -0,0 +1,9 @@
+import { Outlet, createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/app/_app/manage")({
+ component: ManageRoute,
+});
+
+function ManageRoute() {
+ return ;
+}
diff --git a/apps/web/src/routes/app._app.overview.tsx b/apps/web/src/routes/app._app.overview.tsx
new file mode 100644
index 0000000..13dc654
--- /dev/null
+++ b/apps/web/src/routes/app._app.overview.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import TrackerDashboardPage from "@/features/time-tracker/pages/tracker-dashboard-page";
+
+export const Route = createFileRoute("/app/_app/overview")({
+ component: TrackerDashboardPage,
+});
diff --git a/apps/web/src/routes/app._app.projects.tsx b/apps/web/src/routes/app._app.projects.tsx
index 6c07dff..c406230 100644
--- a/apps/web/src/routes/app._app.projects.tsx
+++ b/apps/web/src/routes/app._app.projects.tsx
@@ -1,6 +1,7 @@
-import { createFileRoute } from "@tanstack/react-router";
-import ProjectsPage from "@/features/projects/pages/projects-page";
+import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/app/_app/projects")({
- component: ProjectsPage,
+ beforeLoad: () => {
+ redirect({ to: "/app/manage/projects", throw: true });
+ },
});
diff --git a/apps/web/src/routes/app._app.tags.tsx b/apps/web/src/routes/app._app.tags.tsx
index 62e7053..2355e06 100644
--- a/apps/web/src/routes/app._app.tags.tsx
+++ b/apps/web/src/routes/app._app.tags.tsx
@@ -1,6 +1,7 @@
-import { createFileRoute } from "@tanstack/react-router";
-import TagsPage from "@/features/tags/pages/tags-page";
+import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/app/_app/tags")({
- component: TagsPage,
+ beforeLoad: () => {
+ redirect({ to: "/app/manage/tags", throw: true });
+ },
});
diff --git a/apps/web/src/routes/app._app.teams.tsx b/apps/web/src/routes/app._app.teams.tsx
index 09bfd93..d5841a3 100644
--- a/apps/web/src/routes/app._app.teams.tsx
+++ b/apps/web/src/routes/app._app.teams.tsx
@@ -1,6 +1,7 @@
-import { createFileRoute } from "@tanstack/react-router";
-import TeamsPage from "@/features/organization/pages/teams-page";
+import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/app/_app/teams")({
- component: TeamsPage,
+ beforeLoad: () => {
+ redirect({ to: "/app/manage/teams", throw: true });
+ },
});
diff --git a/apps/web/src/routes/app._app.tracker.tsx b/apps/web/src/routes/app._app.tracker.tsx
index ea56f70..d9b6a7d 100644
--- a/apps/web/src/routes/app._app.tracker.tsx
+++ b/apps/web/src/routes/app._app.tracker.tsx
@@ -1,7 +1,7 @@
-import { createFileRoute } from "@tanstack/react-router";
-
-import TimeTrackerPage from "@/features/time-tracker/pages/time-tracker-page";
+import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/app/_app/tracker")({
- component: TimeTrackerPage,
+ beforeLoad: () => {
+ redirect({ to: "/app", throw: true });
+ },
});
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
index e4d77ab..ff66f18 100644
--- a/apps/web/src/routes/index.tsx
+++ b/apps/web/src/routes/index.tsx
@@ -7,12 +7,11 @@ export const Route = createFileRoute("/")({
head: () => ({
meta: [
{
- title: "Open Clock | Home",
+ title: "better clock | Home",
},
{
name: "description",
- content:
- "Track time with clear project context, honest billable reporting, and a calm workspace.",
+ content: "Track time with clear project context and a calm workspace.",
},
],
}),
diff --git a/apps/web/src/utils/format.ts b/apps/web/src/utils/format.ts
new file mode 100644
index 0000000..9992e79
--- /dev/null
+++ b/apps/web/src/utils/format.ts
@@ -0,0 +1,17 @@
+/**
+ * Formats a duration in seconds as "Xh YYm" for use in table cells.
+ * Example: 5400 → "1h 30m"
+ */
+export function formatDurationHM(totalSeconds: number): string {
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ return `${hours}h ${String(minutes).padStart(2, "0")}m`;
+}
+
+/**
+ * Formats a monetary amount with its currency code.
+ * Example: (1234.5, "USD") → "1234.50 USD"
+ */
+export function formatCurrencyAmount(amount: number, currency: string | null): string {
+ return `${amount.toFixed(2)} ${currency ?? "USD"}`;
+}
diff --git a/packages/auth/src/lib/email.ts b/packages/auth/src/lib/email.ts
index 4476668..3fdf619 100644
--- a/packages/auth/src/lib/email.ts
+++ b/packages/auth/src/lib/email.ts
@@ -35,16 +35,16 @@ export async function sendInvitationEmail({
const safeOrgName = escapeHtml(orgName);
await transporter.sendMail({
- from: `"Open Clock" <${env.GMAIL_USER}>`,
+ from: `"better clock" <${env.GMAIL_USER}>`,
to,
- subject: `You've been invited to join ${orgName} on Open Clock`,
+ subject: `You've been invited to join ${orgName} on better clock`,
html: `