diff --git a/e2e/timetracker.spec.ts b/e2e/timetracker.spec.ts
index 4d294fb5..7b19b197 100644
--- a/e2e/timetracker.spec.ts
+++ b/e2e/timetracker.spec.ts
@@ -115,7 +115,6 @@ test('test that starting and updating the time while running works', async ({
(await response.json()).data.project_id === null &&
(await response.json()).data.description === '' &&
(await response.json()).data.task_id === null &&
- (await response.json()).data.duration === null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify([])
diff --git a/e2e/utils/currentTimeEntry.ts b/e2e/utils/currentTimeEntry.ts
index f73faa68..5de2771b 100644
--- a/e2e/utils/currentTimeEntry.ts
+++ b/e2e/utils/currentTimeEntry.ts
@@ -27,7 +27,6 @@ export function newTimeEntryResponse(
(await response.json()).data.project_id === null &&
(await response.json()).data.description === description &&
(await response.json()).data.task_id === null &&
- (await response.json()).data.duration === null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify(tags)
diff --git a/openapi.json.client.ts b/openapi.json.client.ts
index e6fafb4b..696268cd 100644
--- a/openapi.json.client.ts
+++ b/openapi.json.client.ts
@@ -118,7 +118,7 @@ const TaskResource = z
const createTask_Body = z
.object({ name: z.string(), project_id: z.string() })
.passthrough();
-const before = z.union([z.string(), z.null()]).optional();
+const start = z.union([z.string(), z.null()]).optional();
const TimeEntryResource = z
.object({
id: z.string(),
@@ -163,22 +163,6 @@ const v1_time_entries_update_multiple_Body = z
.passthrough(),
})
.passthrough();
-const group = z
- .union([
- z.enum([
- 'day',
- 'week',
- 'month',
- 'year',
- 'user',
- 'project',
- 'task',
- 'client',
- 'billable',
- ]),
- z.null(),
- ])
- .optional();
const updateTimeEntry_Body = z
.object({
member_id: z.string().uuid().optional(),
@@ -213,12 +197,11 @@ export const schemas = {
TagCollection,
TaskResource,
createTask_Body,
- before,
+ start,
TimeEntryResource,
TimeEntryCollection,
createTimeEntry_Body,
v1_time_entries_update_multiple_Body,
- group,
updateTimeEntry_Body,
};
@@ -1776,14 +1759,14 @@ Users with the permission `time-entries:view:own` can only use this en
schema: z.string().uuid().optional(),
},
{
- name: 'before',
+ name: 'start',
type: 'Query',
- schema: before,
+ schema: start,
},
{
- name: 'after',
+ name: 'end',
type: 'Query',
- schema: before,
+ schema: start,
},
{
name: 'active',
@@ -2055,12 +2038,36 @@ If the group parameters are all set to `null` or are all missing, the
{
name: 'group',
type: 'Query',
- schema: group,
+ schema: z
+ .enum([
+ 'day',
+ 'week',
+ 'month',
+ 'year',
+ 'user',
+ 'project',
+ 'task',
+ 'client',
+ 'billable',
+ ])
+ .optional(),
},
{
name: 'sub_group',
type: 'Query',
- schema: group,
+ schema: z
+ .enum([
+ 'day',
+ 'week',
+ 'month',
+ 'year',
+ 'user',
+ 'project',
+ 'task',
+ 'client',
+ 'billable',
+ ])
+ .optional(),
},
{
name: 'member_id',
@@ -2073,14 +2080,14 @@ If the group parameters are all set to `null` or are all missing, the
schema: z.string().uuid().optional(),
},
{
- name: 'before',
+ name: 'start',
type: 'Query',
- schema: before,
+ schema: start,
},
{
- name: 'after',
+ name: 'end',
type: 'Query',
- schema: before,
+ schema: start,
},
{
name: 'active',
@@ -2092,6 +2099,11 @@ If the group parameters are all set to `null` or are all missing, the
type: 'Query',
schema: z.enum(['true', 'false']).optional(),
},
+ {
+ name: 'fill_gaps_in_time_groups',
+ type: 'Query',
+ schema: z.enum(['true', 'false']).optional(),
+ },
{
name: 'member_ids',
type: 'Query',
@@ -2117,19 +2129,22 @@ If the group parameters are all set to `null` or are all missing, the
.object({
data: z
.object({
+ grouped_type: z.union([z.string(), z.null()]),
grouped_data: z.union([
z.array(
z
.object({
- type: z.string(),
key: z.union([z.string(), z.null()]),
seconds: z.number().int(),
cost: z.number().int(),
+ grouped_type: z.union([
+ z.string(),
+ z.null(),
+ ]),
grouped_data: z.union([
z.array(
z
.object({
- type: z.string(),
key: z.union([
z.string(),
z.null(),
@@ -2138,6 +2153,8 @@ If the group parameters are all set to `null` or are all missing, the
.number()
.int(),
cost: z.number().int(),
+ grouped_type: z.null(),
+ grouped_data: z.null(),
})
.passthrough()
),
diff --git a/resources/js/Components/Common/Reporting/ReportingChart.vue b/resources/js/Components/Common/Reporting/ReportingChart.vue
index a8cb50eb..c171dc75 100644
--- a/resources/js/Components/Common/Reporting/ReportingChart.vue
+++ b/resources/js/Components/Common/Reporting/ReportingChart.vue
@@ -2,7 +2,11 @@
import VChart, { THEME_KEY } from 'vue-echarts';
import { computed, provide, ref } from 'vue';
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
-import { formatHumanReadableDuration } from '@/utils/time';
+import {
+ formatDate,
+ formatHumanReadableDuration,
+ formatWeek,
+} from '@/utils/time';
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { BarChart } from 'echarts/charts';
@@ -30,10 +34,14 @@ type GroupedData = AggregatedTimeEntries['grouped_data'];
const props = defineProps<{
groupedData: GroupedData;
+ groupedType: string | null;
}>();
const xAxisLabels = computed(() => {
- return props?.groupedData?.map((el) => el.key);
+ if (props.groupedType === 'week') {
+ return props?.groupedData?.map((el) => formatWeek(el.key));
+ }
+ return props?.groupedData?.map((el) => formatDate(el.key ?? ''));
});
const accentColor = useCssVar('--color-accent-quaternary');
@@ -108,9 +116,10 @@ const option = ref({
},
},
axisLabel: {
- fontSize: 16,
+ fontSize: 12,
fontWeight: 600,
- margin: 24,
+ color: 'rgba(255,255,255,0.7)',
+ margin: 16,
fontFamily: 'Outfit, sans-serif',
},
axisTick: {
diff --git a/resources/js/Components/Common/Reporting/ReportingPieChart.vue b/resources/js/Components/Common/Reporting/ReportingPieChart.vue
new file mode 100644
index 00000000..0b40df4a
--- /dev/null
+++ b/resources/js/Components/Common/Reporting/ReportingPieChart.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
diff --git a/resources/js/Components/Common/Reporting/ReportingRow.vue b/resources/js/Components/Common/Reporting/ReportingRow.vue
index ab6cc3fa..38accff0 100644
--- a/resources/js/Components/Common/Reporting/ReportingRow.vue
+++ b/resources/js/Components/Common/Reporting/ReportingRow.vue
@@ -2,14 +2,13 @@
import { formatHumanReadableDuration } from '@/utils/time';
import { formatMoney } from '@/utils/money';
import GroupedItemsCountButton from '@/Components/Common/GroupedItemsCountButton.vue';
-import { computed, ref } from 'vue';
-import { useProjectsStore } from '@/utils/useProjects';
-import { storeToRefs } from 'pinia';
-import { useMembersStore } from '@/utils/useMembers';
-import { useTasksStore } from '@/utils/useTasks';
+import { ref } from 'vue';
import { twMerge } from 'tailwind-merge';
+import { useReportingStore } from '@/utils/useReporting';
+const { getNameForReportingRowEntry } = useReportingStore();
type AggregatedGroupedData = GroupedData & {
+ grouped_type?: string | null;
grouped_data?: GroupedData[] | null;
};
@@ -17,48 +16,16 @@ type GroupedData = {
key: string | null;
seconds: number;
cost: number;
- type: string;
};
const props = defineProps<{
entry: AggregatedGroupedData;
indent?: boolean;
+ type: string | null;
}>();
-const emptyPlaceholder = computed(() => {
- const emptyPlaceholder = {
- user: 'No User',
- project: 'No Project',
- task: 'No Task',
- billable: 'Non-Billable',
- };
-
- return emptyPlaceholder[props.entry.type as keyof typeof emptyPlaceholder];
-});
-
-function getNameForKey(key: string) {
- if (props.entry.type === 'project') {
- const projectsStore = useProjectsStore();
- const { projects } = storeToRefs(projectsStore);
- return projects.value.find((project) => project.id === key)?.name;
- }
- if (props.entry.type === 'user') {
- const memberStore = useMembersStore();
- const { members } = storeToRefs(memberStore);
- return members.value.find((member) => member.user_id === key)?.name;
- }
- if (props.entry.type === 'task') {
- const taskStore = useTasksStore();
- const { tasks } = storeToRefs(taskStore);
- return tasks.value.find((task) => task.id === key)?.name;
- }
- if (props.entry.type === 'billable') {
- if (key === '0') {
- return 'Non-Billable';
- } else {
- return 'Billable';
- }
- }
+function getNameForKey(key: string | null) {
+ return getNameForReportingRowEntry(key, props.type);
}
const expanded = ref(false);
@@ -80,7 +47,7 @@ const expanded = ref(false);
{{ entry.grouped_data?.length }}
- {{ entry.key ? getNameForKey(entry.key) : emptyPlaceholder }}
+ {{ getNameForKey(entry.key) }}
@@ -97,6 +64,7 @@ const expanded = ref(false);
diff --git a/resources/js/Components/CurrentSidebarTimer.vue b/resources/js/Components/CurrentSidebarTimer.vue
index 296a4342..e6bf1fa7 100644
--- a/resources/js/Components/CurrentSidebarTimer.vue
+++ b/resources/js/Components/CurrentSidebarTimer.vue
@@ -30,7 +30,7 @@ const isRunningInDifferentOrganization = computed(() => {
-
+
@@ -44,7 +44,7 @@ const isRunningInDifferentOrganization = computed(() => {
-
diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue
index 45f5de3b..4a98a864 100644
--- a/resources/js/Layouts/AppLayout.vue
+++ b/resources/js/Layouts/AppLayout.vue
@@ -74,7 +74,7 @@ const page = usePage<{
-