Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/Http/Controllers/Api/V1/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public function update(Organization $organization, Report $report, ReportUpdateR
$report->share_secret = null;
$report->public_until = null;
}
} elseif ($report->is_public && $request->has('public_until')) {
$report->public_until = $request->getPublicUntil();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@korridor das update von public_until hat hier davor nicht funktioniert, außer wenn gleichzeitig public geändert wurde. passt das so oder überseh ich da wasß

}
$report->save();

Expand Down
4 changes: 3 additions & 1 deletion e2e/organization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ test('test that format settings are reflected in the dashboard', async ({
await expect(page.getByText('0.00€')).toBeVisible();

// check that 00:00 is displayed
await expect(page.getByText('0:00', { exact: true }).nth(0)).toBeVisible();
await expect(
page.getByText('0:00 h', { exact: true }).nth(0)
).toBeVisible();
// check that 0h 00min is not displayed
await expect(
page.getByText('0h 00min', { exact: true }).nth(0)
Expand Down
45 changes: 44 additions & 1 deletion e2e/projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test('test that updating billable rate works with existing time entries', async

await page.getByRole('row').first().getByRole('button').click();
await page.getByRole('menuitem').getByText('Edit').first().click();
await page.getByText('Non-Billable').click();
await page.getByText('Non-Billable').click();
await page.getByText('Custom Rate').click();
await page
.getByPlaceholder('Billable Rate')
Expand Down Expand Up @@ -136,6 +136,49 @@ test('test that updating billable rate works with existing time entries', async
).toBeVisible();
});

test('test that creating and updating project time estimate works', async ({ page }) => {
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
const timeEstimate = '10';

await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Project' }).click();
await page.getByLabel('Project Name').fill(newProjectName);
await page.getByLabel('Time Estimated').fill(timeEstimate);

await Promise.all([
page.getByRole('button', { name: 'Create Project' }).click(),
page.waitForResponse(
async (response) =>
response.url().includes('/projects') &&
response.request().method() === 'POST' &&
response.status() === 201 &&
(await response.json()).data.estimated_time === parseInt(timeEstimate) * 60 * 60
),
]);

// Check that time estimate is displayed in the projects table
await expect(page.getByTestId('project_table')).toContainText(timeEstimate + 'h');

// Edit project to remove time estimate
await page.getByRole('row').first().getByRole('button').click();
await page.getByRole('menuitem').getByText('Edit').first().click();
await page.getByLabel('Time Estimated').fill('');

await Promise.all([
page.getByRole('button', { name: 'Update Project' }).click(),
page.waitForResponse(
async (response) =>
response.url().includes('/projects') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.estimated_time === null
),
]);

// Check that time estimate is no longer displayed
await expect(page.getByTestId('project_table')).not.toContainText(timeEstimate + 'h');
});

// Create new project with new Client

// Create new project with existing Client
Expand Down
14 changes: 11 additions & 3 deletions resources/js/Components/Common/Report/ReportCreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { api } from '@/packages/api/src';
import { Checkbox } from '@/packages/ui/src';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import { useNotificationsStore } from '@/utils/notification';
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';

const show = defineModel('show', { default: false });
const saving = ref(false);
Expand Down Expand Up @@ -47,10 +48,14 @@ const report = ref({
const { handleApiRequestNotifications } = useNotificationsStore();

async function submit() {
const { public_until, ...reportProperties } = report.value;
await handleApiRequestNotifications(
() =>
createReportMutation.mutateAsync({
...report.value,
...reportProperties,
public_until: public_until
? getLocalizedDayJs(public_until).utc().format()
: null,
properties: { ...props.properties },
}),
'Success',
Expand Down Expand Up @@ -103,13 +108,16 @@ async function submit() {
<div
v-if="report.is_public"
class="flex items-center space-x-4">
<div>
<div class="w-full">
<InputLabel for="public_until" value="Expires at" />
<div class="text-text-tertiary font-medium">
(optional)
</div>
</div>
<DatePicker id="public_until"></DatePicker>
<DatePicker
id="public_until"
v-model="report.public_until"
size="input"></DatePicker>
</div>
</div>
</div>
Expand Down
15 changes: 13 additions & 2 deletions resources/js/Components/Common/Report/ReportEditModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Checkbox } from '@/packages/ui/src';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import { useNotificationsStore } from '@/utils/notification';
import type { Report } from '@/packages/api/src';
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';

const show = defineModel('show', { default: false });
const saving = ref(false);
Expand Down Expand Up @@ -64,8 +65,15 @@ watch(
const { handleApiRequestNotifications } = useNotificationsStore();

async function submit() {
const { public_until, ...reportProperties } = report.value;
await handleApiRequestNotifications(
() => updateReportMutation.mutateAsync(report.value),
() =>
updateReportMutation.mutateAsync({
...reportProperties,
public_until: public_until
? getLocalizedDayJs(public_until).utc().format()
: null,
}),
'Success',
'Error',
() => {
Expand Down Expand Up @@ -118,7 +126,10 @@ async function submit() {
v-if="report.is_public"
class="flex items-center space-x-4">
<InputLabel for="public_until" value="Expires at" />
<DatePicker id="public_until"></DatePicker>
<DatePicker
id="public_until"
v-model="report.public_until"
size="input"></DatePicker>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions resources/js/Pages/Time.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ function deleteSelected() {
</script>

<template>
<TimeEntryCreateModal
<AppLayout title="Dashboard" data-testid="time_view">
<TimeEntryCreateModal
v-model:show="showManualTimeEntryModal"
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:create-project="createProject"
Expand All @@ -130,7 +131,6 @@ function deleteSelected() {
:tasks
:tags
:clients></TimeEntryCreateModal>
<AppLayout title="Dashboard" data-testid="time_view">
<MainContainer
class="pt-5 lg:pt-8 pb-4 lg:pb-6">
<div
Expand Down
3 changes: 2 additions & 1 deletion resources/js/packages/ui/src/EstimatedTimeSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ const emit = defineEmits(['submit']);
<div class="pt-6">
<div class="flex items-center space-x-1 mb-2">
<ClockIcon class="text-text-quaternary w-4"></ClockIcon>
<InputLabel for="billable" value="Time Estimated" />
<InputLabel for="time-estimated" value="Time Estimated" />
</div>
<DurationInput
id="time-estimated"
v-model="model"
class="max-w-[150px]"
@submit="emit('submit')"></DurationInput>
Expand Down
138 changes: 79 additions & 59 deletions resources/js/packages/ui/src/Input/DatePicker.vue
Original file line number Diff line number Diff line change
@@ -1,76 +1,96 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import {
getDayJsInstance,
getLocalizedDayJs,
} from '@/packages/ui/src/utils/time';
import { twMerge } from 'tailwind-merge';
Popover,
PopoverContent,
PopoverTrigger,
} from '@/Components/ui/popover';
import { Button, type ButtonVariants } from '@/Components/ui/button';
import { Calendar } from '@/Components/ui/calendar';
import { CalendarIcon } from 'lucide-vue-next';
import { formatDateLocalized } from '@/packages/ui/src/utils/time';
import { parseDate, type DateValue } from '@internationalized/date';
import { computed, inject, type ComputedRef } from 'vue';
import { type Organization } from '@/packages/api/src';
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';

const props = defineProps<{
class?: string;
tabindex?: string;
size: ButtonVariants['size'];
}>();

// This has to be a localized timestamp, not UTC
const model = defineModel<string | null>({
default: null,
});

const tempDate = ref(getLocalizedDayJs(model.value).format('YYYY-MM-DD'));

watch(model, (value) => {
tempDate.value = getLocalizedDayJs(value).format('YYYY-MM-DD');
});
const model = defineModel<string | null>();
const emit = defineEmits<{
changed: [string];
}>();

function updateDate(event: Event) {
const target = event.target as HTMLInputElement;
const newValue = target.value;
const newDate = getDayJsInstance()(newValue);
if (newDate.isValid()) {
model.value = getLocalizedDayJs(model.value)
.set('year', newDate.year())
.set('month', newDate.month())
.set('date', newDate.date())
.format();
emit('changed', model.value);
const handleChange = (date: DateValue | undefined) => {
if (!date) {
model.value = null;
return;
}
}

const datePicker = ref<HTMLInputElement | null>(null);
const dayjs = model.value
? getLocalizedDayJs(model.value)
: getLocalizedDayJs();
model.value = dayjs
.year(date.year)
.month(date.month - 1) // CalendarDate uses 1-based months
.date(date.day)
.format();
emit('changed', model.value);
};

function updateTempValue(event: Event) {
const target = event.target as HTMLInputElement;
tempDate.value = target.value;
}
const date = computed(() => {
return model.value
? parseDate(getLocalizedDayJs(model.value).format('YYYY-MM-DD'))
: undefined;
});

const emit = defineEmits(['changed']);
const organization = inject<ComputedRef<Organization>>('organization');
</script>

<template>
<div class="flex items-center text-text-secondary">
<input
id="start"
ref="datePicker"
:tabindex="tabindex"
:class="
twMerge(
'bg-input-background border text-text-primary border-input-border focus-visible:outline-0 focus-visible:border-input-border-active focus-visible:ring-0 rounded-md',
props.class
)
"
type="date"
name="trip-start"
:value="tempDate"
@change="updateTempValue"
@blur="updateDate"
@keydown.enter="updateDate" />
</div>
<Popover>
<PopoverTrigger as-child>
<Button
variant="input"
:size="size"
:class="[
size === 'sm' ? 'gap-1.5' : 'gap-2',
'w-full justify-center text-left font-normal',
!model && 'text-muted-foreground',
props.class,
]"
:tabindex="tabindex">
<CalendarIcon
:class="[
size === 'xs'
? 'h-3 w-3'
: size === 'sm'
? 'h-3 w-3'
: size === 'lg'
? 'h-4.5 w-4.5'
: 'h-4 w-4',
]" />
<span class="text-center">
{{
model
? formatDateLocalized(
model,
organization?.date_format
)
: 'Pick a date'
}}
</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar
mode="single"
:model-value="date"
:initial-focus="true"
@update:model-value="handleChange" />
</PopoverContent>
</Popover>
</template>

<style scoped>
input::-webkit-calendar-picker-indicator {
filter: invert(1);

opacity: 0.2;
}
</style>
7 changes: 7 additions & 0 deletions resources/js/packages/ui/src/Input/DurationInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import { computed, ref } from 'vue';
import { TextInput } from '@/packages/ui/src';

defineProps<{
id?: string;
}>();

const model = defineModel<number | null>({
default: null,
});
Expand All @@ -16,6 +20,8 @@ function updateDuration() {
const hours = parseInt(temporaryCustomTimerEntry.value);
if (!isNaN(hours)) {
model.value = hours * 60 * 60;
} else {
model.value = null;
}
temporaryCustomTimerEntry.value = '';
}
Expand Down Expand Up @@ -54,6 +60,7 @@ function updateAndSubmit() {
<template>
<div class="relative">
<TextInput
:id="id"
v-model="currentTime"
class="w-full overflow-hidden pr-14"
placeholder="0"
Expand Down
2 changes: 2 additions & 0 deletions resources/js/packages/ui/src/Input/TextInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { twMerge } from 'tailwind-merge';
const props = defineProps<{
name?: string;
class?: string;
id?: string;
}>();

const input = ref<HTMLInputElement | null>(null);
Expand All @@ -21,6 +22,7 @@ const model = defineModel();

<template>
<input
:id="id"
ref="input"
v-model="model"
:class="
Expand Down
Loading
Loading