mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 13:12:16 +01:00
add daily duration to header, fix dropdown overflows, add time dropdown to duration select
This commit is contained in:
@@ -1,34 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatHumanReadableDate,
|
formatHumanReadableDuration,
|
||||||
|
formatWeekday,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
date: string;
|
date: string;
|
||||||
|
duration: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex justify-between items-center">
|
||||||
<svg
|
<div class="flex items-center space-x-2">
|
||||||
class="w-4 sm:w-5 text-muted"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
class="w-4 sm:w-5 text-muted"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
viewBox="0 0 24 24"
|
||||||
<g fill="none">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<g fill="none">
|
||||||
d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
<path
|
||||||
<path
|
d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||||
fill="currentColor"
|
<path
|
||||||
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7zm-5-9a1 1 0 0 1 1 1v1h2a2 2 0 0 1 2 2v3H3V7a2 2 0 0 1 2-2h2V4a1 1 0 0 1 2 0v1h6V4a1 1 0 0 1 1-1" />
|
fill="currentColor"
|
||||||
</g>
|
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7zm-5-9a1 1 0 0 1 1 1v1h2a2 2 0 0 1 2 2v3H3V7a2 2 0 0 1 2-2h2V4a1 1 0 0 1 2 0v1h6V4a1 1 0 0 1 1-1" />
|
||||||
</svg>
|
</g>
|
||||||
<span class="font-semibold text-white">
|
</svg>
|
||||||
{{ formatHumanReadableDate(date) }}
|
<span class="font-semibold text-white">
|
||||||
</span>
|
{{ formatWeekday(date) }}
|
||||||
<span class="font-semibold text-muted">
|
</span>
|
||||||
{{ formatDate(date) }}
|
<span class="font-semibold text-muted">
|
||||||
</span>
|
{{ formatDate(date) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted pr-[90px] lg:pr-[108px]">
|
||||||
|
<span class="font-semibold">
|
||||||
|
{{ formatHumanReadableDuration(duration) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
flip,
|
flip,
|
||||||
|
limitShift,
|
||||||
type Placement,
|
type Placement,
|
||||||
type ReferenceElement,
|
type ReferenceElement,
|
||||||
|
shift,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
import { offset } from '@floating-ui/vue';
|
import { offset } from '@floating-ui/vue';
|
||||||
@@ -59,7 +61,15 @@ const floating = ref(null);
|
|||||||
const { floatingStyles } = useFloating(reference, floating, {
|
const { floatingStyles } = useFloating(reference, floating, {
|
||||||
placement: props.align,
|
placement: props.align,
|
||||||
whileElementsMounted: autoUpdate,
|
whileElementsMounted: autoUpdate,
|
||||||
middleware: [flip(), offset(10)],
|
middleware: [
|
||||||
|
flip(),
|
||||||
|
offset(10),
|
||||||
|
shift({
|
||||||
|
limiter: limitShift({
|
||||||
|
offset: 5,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const props = defineProps<{
|
|||||||
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
|
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
|
||||||
onStartStopClick: (timeEntry: TimeEntry) => void;
|
onStartStopClick: (timeEntry: TimeEntry) => void;
|
||||||
updateTimeEntries: (ids: string[], changes: Partial<TimeEntry>) => void;
|
updateTimeEntries: (ids: string[], changes: Partial<TimeEntry>) => void;
|
||||||
|
updateTimeEntry: (timeEntry: TimeEntry) => void;
|
||||||
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
|
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
|
||||||
currency: string;
|
currency: string;
|
||||||
}>();
|
}>();
|
||||||
@@ -156,8 +157,7 @@ const expanded = ref(false);
|
|||||||
:tags="tags"
|
:tags="tags"
|
||||||
indent
|
indent
|
||||||
:updateTimeEntry="
|
:updateTimeEntry="
|
||||||
(timeEntry: TimeEntry) =>
|
(timeEntry: TimeEntry) => updateTimeEntry(timeEntry)
|
||||||
updateTimeEntries([timeEntry.id], { ...timeEntry })
|
|
||||||
"
|
"
|
||||||
:onStartStopClick="() => onStartStopClick(subEntry)"
|
:onStartStopClick="() => onStartStopClick(subEntry)"
|
||||||
:deleteTimeEntry="() => deleteTimeEntries([subEntry])"
|
:deleteTimeEntry="() => deleteTimeEntries([subEntry])"
|
||||||
|
|||||||
@@ -112,11 +112,16 @@ function startTimeEntryFromExisting(entry: TimeEntry) {
|
|||||||
description: entry.description,
|
description: entry.description,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function sumDuration(timeEntries: TimeEntry[]) {
|
||||||
|
return timeEntries.reduce((acc, entry) => acc + (entry?.duration ?? 0), 0);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(value, key) in groupedTimeEntries" :key="key">
|
<div v-for="(value, key) in groupedTimeEntries" :key="key">
|
||||||
<TimeEntryRowHeading :date="key"></TimeEntryRowHeading>
|
<TimeEntryRowHeading
|
||||||
|
:date="key"
|
||||||
|
:duration="sumDuration(value)"></TimeEntryRowHeading>
|
||||||
<template v-for="entry in value" :key="entry.id">
|
<template v-for="entry in value" :key="entry.id">
|
||||||
<TimeEntryAggregateRow
|
<TimeEntryAggregateRow
|
||||||
:createProject
|
:createProject
|
||||||
@@ -127,6 +132,7 @@ function startTimeEntryFromExisting(entry: TimeEntry) {
|
|||||||
:clients
|
:clients
|
||||||
:onStartStopClick="startTimeEntryFromExisting"
|
:onStartStopClick="startTimeEntryFromExisting"
|
||||||
:updateTimeEntries
|
:updateTimeEntries
|
||||||
|
:updateTimeEntry
|
||||||
:deleteTimeEntries
|
:deleteTimeEntries
|
||||||
:createTag
|
:createTag
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
@@ -145,7 +151,7 @@ function startTimeEntryFromExisting(entry: TimeEntry) {
|
|||||||
:deleteTimeEntry="() => deleteTimeEntries([entry])"
|
:deleteTimeEntry="() => deleteTimeEntries([entry])"
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
v-else
|
v-else
|
||||||
:time-entry="entry"></TimeEntryRow>
|
:time-entry="entry.timeEntries[0]"></TimeEntryRow>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
import { computed, defineProps, ref } from 'vue';
|
import { computed, defineProps, ref } from 'vue';
|
||||||
import parse from 'parse-duration';
|
import parse from 'parse-duration';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
|
||||||
|
import TimeRangeSelector from '@/packages/ui/src/Input/TimeRangeSelector.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
start: string;
|
start: string;
|
||||||
@@ -16,6 +18,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const temporaryCustomTimerEntry = ref<string>('');
|
const temporaryCustomTimerEntry = ref<string>('');
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
function updateTimerAndStartLiveTimerUpdate() {
|
function updateTimerAndStartLiveTimerUpdate() {
|
||||||
const time = parse(temporaryCustomTimerEntry.value, 's');
|
const time = parse(temporaryCustomTimerEntry.value, 's');
|
||||||
@@ -53,19 +56,38 @@ const currentTime = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function selectInput(event: Event) {
|
function selectInput(event: Event) {
|
||||||
|
open.value = true;
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
target.select();
|
target.select();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input
|
<Dropdown
|
||||||
data-testid="time_entry_duration_input"
|
v-model="open"
|
||||||
class="text-white w-[100px] px-3 py-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold"
|
@submit="open = false"
|
||||||
@focus="selectInput"
|
align="bottom"
|
||||||
@blur="updateTimerAndStartLiveTimerUpdate"
|
:close-on-content-click="false">
|
||||||
@keydown.enter="updateTimerAndStartLiveTimerUpdate"
|
<template #trigger>
|
||||||
v-model="currentTime" />
|
<input
|
||||||
|
data-testid="time_entry_duration_input"
|
||||||
|
class="text-white w-[100px] px-3 py-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold"
|
||||||
|
@focus="selectInput"
|
||||||
|
@blur="updateTimerAndStartLiveTimerUpdate"
|
||||||
|
@keydown.enter="updateTimerAndStartLiveTimerUpdate"
|
||||||
|
v-model="currentTime" />
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<TimeRangeSelector
|
||||||
|
@changed="
|
||||||
|
(newStart: string, newEnd: string) =>
|
||||||
|
emit('changed', newStart, newEnd)
|
||||||
|
"
|
||||||
|
:start="start"
|
||||||
|
:end="end">
|
||||||
|
</TimeRangeSelector>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import DaySectionHeader from '@/packages/ui/src/DaySectionHeader.vue';
|
|||||||
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||||
defineProps<{
|
defineProps<{
|
||||||
date: string;
|
date: string;
|
||||||
|
duration: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ defineProps<{
|
|||||||
<div
|
<div
|
||||||
class="bg-card-background border-t border-b border-card-border py-1 lg:py-1.5 text-xs sm:text-sm">
|
class="bg-card-background border-t border-b border-card-border py-1 lg:py-1.5 text-xs sm:text-sm">
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<DaySectionHeader :date></DaySectionHeader>
|
<DaySectionHeader :date :duration></DaySectionHeader>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ export function formatHumanReadableDate(date: string) {
|
|||||||
return dayjs(date).fromNow();
|
return dayjs(date).fromNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatWeekday(date: string) {
|
||||||
|
return dayjs(date).format('dddd');
|
||||||
|
}
|
||||||
|
|
||||||
export function formatStartEnd(start: string, end: string | null) {
|
export function formatStartEnd(start: string, end: string | null) {
|
||||||
if (end) {
|
if (end) {
|
||||||
return `${formatTime(start)} - ${formatTime(end)}`;
|
return `${formatTime(start)} - ${formatTime(end)}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user