mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 05:22:44 +01:00
Compare commits
5 Commits
b660486eb7
...
7f75e5f1b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f75e5f1b5 | ||
|
|
8941aec72b | ||
|
|
bc1f4deaa0 | ||
|
|
d292396dd4 | ||
|
|
f926e8fde3 |
@@ -62,7 +62,7 @@ class TimeEntryFilter
|
||||
if ($start === null) {
|
||||
return $this;
|
||||
}
|
||||
$this->builder->where('start', '>', $start);
|
||||
$this->builder->where('start', '>=', $start);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ const emit = defineEmits<{
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:organization-billable-rate="organization?.billable_rate ?? null"
|
||||
:no-project-value="null"
|
||||
@changed="(p, t) => emit('add-row', p, t)">
|
||||
<template #trigger>
|
||||
<Button variant="ghost" size="sm" class="text-text-secondary">
|
||||
|
||||
@@ -81,6 +81,7 @@ function hasRunningEntry(dayIndex: number): boolean {
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:organization-billable-rate="organization?.billable_rate ?? null"
|
||||
:no-project-value="null"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-full" />
|
||||
|
||||
@@ -16,6 +16,8 @@ import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { Button } from '@/packages/ui/src/Buttons';
|
||||
|
||||
const NO_PROJECT_ID = '';
|
||||
|
||||
const task = defineModel<string | null>('task', {
|
||||
default: null,
|
||||
});
|
||||
@@ -57,6 +59,7 @@ const props = withDefaults(
|
||||
currency: string;
|
||||
emptyPlaceholder?: string;
|
||||
allowReset?: boolean;
|
||||
noProjectValue?: string | null;
|
||||
enableEstimatedTime: boolean;
|
||||
organizationBillableRate: number | null;
|
||||
canCreateProject: boolean;
|
||||
@@ -68,6 +71,7 @@ const props = withDefaults(
|
||||
{
|
||||
emptyPlaceholder: 'No Project',
|
||||
allowReset: false,
|
||||
noProjectValue: NO_PROJECT_ID,
|
||||
variant: 'ghost',
|
||||
align: 'center',
|
||||
size: 'sm',
|
||||
@@ -164,10 +168,10 @@ function updateFilteredResults() {
|
||||
is_archived: false,
|
||||
projects: [
|
||||
{
|
||||
id: '',
|
||||
id: NO_PROJECT_ID,
|
||||
name: 'No Project',
|
||||
color: 'var(--theme-color-icon-default)',
|
||||
value: '',
|
||||
value: NO_PROJECT_ID,
|
||||
client_id: null,
|
||||
billable_rate: null,
|
||||
is_archived: false,
|
||||
@@ -490,7 +494,7 @@ function selectTask(taskId: string) {
|
||||
}
|
||||
|
||||
function selectProject(projectId: string) {
|
||||
project.value = projectId;
|
||||
project.value = projectId === NO_PROJECT_ID ? props.noProjectValue : projectId;
|
||||
task.value = null;
|
||||
open.value = false;
|
||||
searchValue.value = '';
|
||||
|
||||
@@ -26,6 +26,15 @@ interface Interval {
|
||||
end: Dayjs;
|
||||
}
|
||||
|
||||
function localDayBounds(date: string, tz: string): { dayStart: Dayjs; dayEnd: Dayjs } {
|
||||
const dayjs = getDayJsInstance();
|
||||
const localDayStart = dayjs.tz(`${date} 00:00:00`, tz);
|
||||
return {
|
||||
dayStart: localDayStart.utc(),
|
||||
dayEnd: localDayStart.add(1, 'day').utc(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect entries that intersect the day `[dayStart, dayEnd)`, clipped
|
||||
* to those bounds. Running entries use `nowDayjs` as their end.
|
||||
@@ -76,8 +85,7 @@ export function findFreeWindowOnDay(
|
||||
if (requiredSeconds <= 0) return null;
|
||||
|
||||
const dayjs = getDayJsInstance();
|
||||
const dayStart = dayjs.tz(`${date} 00:00:00`, tz).utc();
|
||||
const dayEnd = dayStart.add(1, 'day');
|
||||
const { dayStart, dayEnd } = localDayBounds(date, tz);
|
||||
|
||||
if (requiredSeconds > dayEnd.diff(dayStart, 'second')) return null;
|
||||
|
||||
@@ -149,8 +157,7 @@ export function freeGapSecondsAfter(
|
||||
now?: string | Dayjs
|
||||
): number {
|
||||
const dayjs = getDayJsInstance();
|
||||
const dayStart = dayjs.tz(`${date} 00:00:00`, tz).utc();
|
||||
const dayEnd = dayStart.add(1, 'day');
|
||||
const { dayStart, dayEnd } = localDayBounds(date, tz);
|
||||
const cursorDjs = dayjs.utc(cursor);
|
||||
|
||||
if (cursorDjs.isSameOrAfter(dayEnd)) return 0;
|
||||
|
||||
@@ -10,12 +10,57 @@ use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\TimeEntryFilter;
|
||||
use Illuminate\Support\Carbon;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(TimeEntryFilter::class)]
|
||||
class TimeEntryFilterTest extends TestCaseWithDatabase
|
||||
{
|
||||
public function test_add_start_is_inclusive_of_boundary(): void
|
||||
{
|
||||
// Arrange
|
||||
$boundary = Carbon::parse('2024-01-01 12:00:00', 'UTC');
|
||||
$entryAtBoundary = TimeEntry::factory()->start($boundary)->create();
|
||||
$entryAfterBoundary = TimeEntry::factory()->start($boundary->copy()->addSecond())->create();
|
||||
$entryBeforeBoundary = TimeEntry::factory()->start($boundary->copy()->subSecond())->create();
|
||||
|
||||
$builder = TimeEntry::query();
|
||||
$filter = new TimeEntryFilter($builder);
|
||||
|
||||
// Act
|
||||
$filter->addStart($boundary);
|
||||
|
||||
// Assert
|
||||
$timeEntries = $builder->get();
|
||||
$this->assertCount(2, $timeEntries);
|
||||
$this->assertTrue($timeEntries->contains($entryAtBoundary));
|
||||
$this->assertTrue($timeEntries->contains($entryAfterBoundary));
|
||||
$this->assertFalse($timeEntries->contains($entryBeforeBoundary));
|
||||
}
|
||||
|
||||
public function test_add_end_is_exclusive_of_boundary(): void
|
||||
{
|
||||
// Arrange
|
||||
$boundary = Carbon::parse('2024-01-01 12:00:00', 'UTC');
|
||||
$entryAtBoundary = TimeEntry::factory()->start($boundary)->create();
|
||||
$entryAfterBoundary = TimeEntry::factory()->start($boundary->copy()->addSecond())->create();
|
||||
$entryBeforeBoundary = TimeEntry::factory()->start($boundary->copy()->subSecond())->create();
|
||||
|
||||
$builder = TimeEntry::query();
|
||||
$filter = new TimeEntryFilter($builder);
|
||||
|
||||
// Act
|
||||
$filter->addEnd($boundary);
|
||||
|
||||
// Assert
|
||||
$timeEntries = $builder->get();
|
||||
$this->assertCount(1, $timeEntries);
|
||||
$this->assertTrue($timeEntries->contains($entryBeforeBoundary));
|
||||
$this->assertFalse($timeEntries->contains($entryAtBoundary));
|
||||
$this->assertFalse($timeEntries->contains($entryAfterBoundary));
|
||||
}
|
||||
|
||||
public function test_add_tag_ids_filter_is_or(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Reference in New Issue
Block a user