add table group chart, adapt to api changes

This commit is contained in:
Gregor Vostrak
2024-05-21 18:40:03 +02:00
parent 2e710795ec
commit 867d6eff18
15 changed files with 506 additions and 102 deletions

View File

@@ -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: {

View File

@@ -0,0 +1,134 @@
<script setup lang="ts">
import VChart, { THEME_KEY } from 'vue-echarts';
import { computed, provide, ref } from 'vue';
import LinearGradient from 'zrender/lib/graphic/LinearGradient';
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { PieChart } from 'echarts/charts';
import {
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from 'echarts/components';
import { useCssVar } from '@vueuse/core';
import { formatHumanReadableDuration } from '@/utils/time';
import { getRandomColorWithSeed } from '@/utils/color';
import type { GroupedDataEntries } from '@/utils/api';
import { useReportingStore } from '@/utils/useReporting';
use([
CanvasRenderer,
PieChart,
TitleComponent,
GridComponent,
TooltipComponent,
LegendComponent,
]);
provide(THEME_KEY, 'dark');
const backgroundColor = useCssVar('--theme-color-default-background');
function hexToRGBA(hex: string, opacity = 1) {
// Remove the hash at the start if it's there
hex = hex.replace(/^#/, '');
// Parse the hex color
let r, g, b;
if (hex.length === 3) {
r = parseInt(hex.charAt(0) + hex.charAt(0), 16);
g = parseInt(hex.charAt(1) + hex.charAt(1), 16);
b = parseInt(hex.charAt(2) + hex.charAt(2), 16);
} else if (hex.length === 6) {
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
} else {
throw new Error('Invalid HEX color.');
}
// Return the RGBA color string
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
const props = defineProps<{
data: GroupedDataEntries | null;
type: string | null;
}>();
const { getNameForReportingRowEntry } = useReportingStore();
const groupChartData = computed(() => {
return (
props?.data?.map((entry) => {
return {
value: entry.seconds,
name: getNameForReportingRowEntry(entry.key, props.type),
color: getRandomColorWithSeed(entry.key ?? 'none'),
};
}) ?? []
);
});
const seriesData = computed(() => {
return groupChartData.value.map((el) => {
return {
...el,
...{
itemStyle: {
borderRadius: 15,
// TODO: Fix dynamic color
borderColor: backgroundColor.value,
borderWidth: 18,
color: new LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: hexToRGBA(el.color, 0.8),
},
{
offset: 1,
color: hexToRGBA(el.color, 0.4),
},
]),
},
},
};
});
});
const option = ref({
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
bottom: 'bottom',
},
backgroundColor: 'transparent',
series: [
{
label: {
show: false,
},
tooltip: {
valueFormatter: (value: number) => {
return formatHumanReadableDuration(value);
},
},
data: seriesData,
radius: ['30%', '65%'],
type: 'pie',
},
],
});
</script>
<template>
<v-chart class="chart" :autoresize="true" :option="option" />
</template>
<style scoped>
.chart {
height: 300px;
background: transparent;
}
</style>

View File

@@ -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);
</script>
@@ -80,7 +47,7 @@ const expanded = ref(false);
{{ entry.grouped_data?.length }}
</GroupedItemsCountButton>
<span>
{{ entry.key ? getNameForKey(entry.key) : emptyPlaceholder }}
{{ getNameForKey(entry.key) }}
</span>
</div>
<div class="justify-end flex items-center">
@@ -97,6 +64,7 @@ const expanded = ref(false);
<ReportingRow
indent
v-for="subEntry in entry.grouped_data"
:type="entry?.grouped_type ?? null"
:key="subEntry.key ?? 'none'"
:entry="subEntry"></ReportingRow>
</div>

View File

@@ -30,7 +30,7 @@ const isRunningInDifferentOrganization = computed(() => {
</script>
<template>
<div class="py-4 px-2 flex justify-between items-center relative">
<div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative">
<div
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center"
v-if="isRunningInDifferentOrganization">
@@ -44,7 +44,7 @@ const isRunningInDifferentOrganization = computed(() => {
</div>
<div>
<div class="text-muted font-extrabold text-xs">Current Timer</div>
<div class="text-white font-medium text-lg py-1">
<div class="text-white font-medium text-lg">
{{ currentTime }}
</div>
</div>