Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
47c2d8e
upgrade inertia v2; add prefetching; migrate queries to tanstack query
Onatcer Jan 9, 2026
6f68bbb
refactor timeentries queries and mutations, improve activitygraph, ad…
Onatcer Jan 14, 2026
ebbc4e6
use tanstack query in ProjectMultiselectDropdown, ClientTableRow and …
Onatcer Jan 14, 2026
900ee29
fix e2e project filtering in reporting e2e test
Onatcer Jan 14, 2026
b2a04c8
load time entries above pagination limit for calendar, fixes #995
Onatcer Jan 14, 2026
79999fd
remove redundant projects pinia store after tanstack query migration
Onatcer Jan 14, 2026
3fb75ec
add outline and secondary variants to TimeTrackerStartStop button to …
Onatcer Jan 15, 2026
672c243
add command palette
Onatcer Jan 27, 2026
99400ca
fix: display custom billable rate correctly on project detail page
Onatcer Jan 27, 2026
44bcce9
fix styling inconsistencies
Onatcer Jan 27, 2026
bca1e8b
migrate select/multiselect components to Radix Vue primitives
Onatcer Feb 1, 2026
8524e01
refactor: extract ReportingFilterBar and migrate reporting to TanStac…
Onatcer Feb 2, 2026
66dfc51
add no project, no task, no client, no task, no tag support to the API
Onatcer Feb 2, 2026
fe8c7e9
Update openapi api client spec
Onatcer Feb 2, 2026
0d3978a
Add reporting e2e helpers and detailed tests
Onatcer Feb 2, 2026
7266272
Add client_ids filter to time entry export
Onatcer Feb 2, 2026
1597b54
Enable npm workspaces and update dependencies
Onatcer Feb 2, 2026
98634f4
fix Y-Label ui regression from echarts update
Onatcer Feb 2, 2026
18989a9
Add Mailpit SMTP and refine Playwright tests
Onatcer Feb 2, 2026
09c3205
Allow NONE filter value to shared reports and add shared-report tests
Onatcer Feb 2, 2026
a58becc
Add calendar query prefetch
Onatcer Feb 3, 2026
03e0377
fix responsive issues in timetracker recently tracked entries dropdown
Onatcer Feb 3, 2026
9be97a8
Improve Time page responsiveness and compact tags, fixes #896
Onatcer Feb 3, 2026
7d068fe
fix admin panel time entry save and update, fixes #997
Onatcer Feb 4, 2026
22f3af2
Make sure that time entry billable status updates when project changes,
Onatcer Feb 4, 2026
f82f5e7
fix desync of checkboxes on the reporting detailed page, fixes #892
Onatcer Feb 4, 2026
6668106
migrate datepickers to shadcn, Fixes #877, #807
Onatcer Feb 5, 2026
d264411
Allow updating public_until on already-public reports
Onatcer Feb 5, 2026
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
7 changes: 6 additions & 1 deletion .env.ci
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ SESSION_DRIVER=database
SESSION_LIFETIME=120

# Mail
MAIL_MAILER=log
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
MAIL_FROM_NAME="solidtime"
MAIL_REPLY_TO_ADDRESS="hello@solidtime.test"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ jobs:
services:
mailpit:
image: 'axllent/mailpit:latest'
ports:
- 1025:1025
- 8025:8025
pgsql_test:
image: postgres:15
env:
Expand Down Expand Up @@ -67,6 +70,7 @@ jobs:
run: npx playwright test
env:
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
MAILPIT_BASE_URL: 'http://localhost:8025'

- name: "Upload test results"
uses: actions/upload-artifact@v4
Expand Down
18 changes: 14 additions & 4 deletions app/Filament/Resources/TimeEntryResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Filament\Resources;

use App\Filament\Resources\TimeEntryResource\Pages;
use App\Models\Member;
use App\Models\TimeEntry;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
Expand All @@ -16,6 +17,7 @@
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

class TimeEntryResource extends Resource
{
Expand Down Expand Up @@ -51,15 +53,23 @@ public static function form(Form $form): Form
->rules([
'after_or_equal:start',
]),
Select::make('user_id')
->relationship(name: 'user', titleAttribute: 'email')
->searchable(['name', 'email'])
Select::make('member_id')
->relationship(
name: 'member',
titleAttribute: 'id',
modifyQueryUsing: fn (Builder $query) => $query->with(['user', 'organization'])
)
->getOptionLabelFromRecordUsing(fn (Member $record): string => $record->user->email.' ('.$record->organization->name.')')
->searchable()
->required(),
Select::make('project_id')
->relationship(name: 'project', titleAttribute: 'name')
->searchable(['name'])
->nullable(),
// TODO
Select::make('task_id')
->relationship(name: 'task', titleAttribute: 'name')
->searchable(['name'])
->nullable(),
]);
}

Expand Down
19 changes: 19 additions & 0 deletions app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,28 @@
namespace App\Filament\Resources\TimeEntryResource\Pages;

use App\Filament\Resources\TimeEntryResource;
use App\Models\Member;
use Filament\Resources\Pages\CreateRecord;

class CreateTimeEntry extends CreateRecord
{
protected static string $resource = TimeEntryResource::class;

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeCreate(array $data): array
{
if (isset($data['member_id'])) {
/** @var Member|null $member */
$member = Member::query()->find($data['member_id']);
if ($member !== null) {
$data['user_id'] = $member->user_id;
$data['organization_id'] = $member->organization_id;
}
}

return $data;
}
}
19 changes: 19 additions & 0 deletions app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Filament\Resources\TimeEntryResource\Pages;

use App\Filament\Resources\TimeEntryResource;
use App\Models\Member;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;

Expand All @@ -19,4 +20,22 @@ protected function getHeaderActions(): array
->icon('heroicon-m-trash'),
];
}

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeSave(array $data): array
{
if (isset($data['member_id'])) {
/** @var Member|null $member */
$member = Member::query()->find($data['member_id']);
if ($member !== null) {
$data['user_id'] = $member->user_id;
$data['organization_id'] = $member->organization_id;
}
}

return $data;
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/Api/V1/ChartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function dailyTrackedHours(Organization $organization, DashboardService $
$this->checkPermission($organization, 'charts:view:own');
$user = $this->user();

$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 60);
$dailyTrackedHours = $dashboardService->getDailyTrackedHours($user, $organization, 100);

return response()->json($dailyTrackedHours);
}
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Controllers/Api/V1/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ public function update(Organization $organization, Report $report, ReportUpdateR
$report->share_secret = null;
$report->public_until = null;
}
} elseif ($report->is_public && $request->has('public_until')) {
// Allow updating expiration date on already-public reports
$report->public_until = $request->getPublicUntil();
}
$report->save();

Expand Down
40 changes: 35 additions & 5 deletions app/Http/Requests/V1/Report/ReportStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
use App\Enums\Weekday;
use App\Http\Requests\V1\BaseFormRequest;
use App\Models\Organization;
use App\Service\TimeEntryFilter;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;

/**
Expand All @@ -23,7 +25,7 @@ class ReportStoreRequest extends BaseFormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule|LegacyValidationRule>>
* @return array<string, array<string|ValidationRule|LegacyValidationRule|\Closure>>
*/
public function rules(): array
{
Expand Down Expand Up @@ -81,7 +83,14 @@ public function rules(): array
],
'properties.client_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
// Filter by project IDs, project IDs are OR combined
'properties.project_ids' => [
Expand All @@ -90,7 +99,14 @@ public function rules(): array
],
'properties.project_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
// Filter by tag IDs, tag IDs are OR combined
'properties.tag_ids' => [
Expand All @@ -99,15 +115,29 @@ public function rules(): array
],
'properties.tag_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
'properties.task_ids' => [
'nullable',
'array',
],
'properties.task_ids.*' => [
'string',
'uuid',
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
if (! Str::isUuid($value)) {
$fail('The '.$attribute.' must be a valid UUID.');
}
},
],
'properties.group' => [
'required',
Expand Down
53 changes: 37 additions & 16 deletions app/Http/Requests/V1/TimeEntry/TimeEntryAggregateExportRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Models\Tag;
use App\Models\Task;
use App\Models\User;
use App\Service\TimeEntryFilter;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
Expand All @@ -30,7 +31,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule>>
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|\Closure>>
*/
public function rules(): array
{
Expand Down Expand Up @@ -94,10 +95,15 @@ public function rules(): array
],
'project_ids.*' => [
'string',
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(),
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid()->validate($attribute, $value, $fail);
},
],
// Filter by client IDs, client IDs are OR combined
'client_ids' => [
Expand All @@ -106,10 +112,15 @@ public function rules(): array
],
'client_ids.*' => [
'string',
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
/** @var Builder<Client> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(),
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
/** @var Builder<Client> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid()->validate($attribute, $value, $fail);
},
],
// Filter by tag IDs, tag IDs are OR combined
'tag_ids' => [
Expand All @@ -118,10 +129,15 @@ public function rules(): array
],
'tag_ids.*' => [
'string',
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(),
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder {
/** @var Builder<Tag> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid()->validate($attribute, $value, $fail);
},
],
// Filter by task IDs, task IDs are OR combined
'task_ids' => [
Expand All @@ -130,9 +146,14 @@ public function rules(): array
],
'task_ids.*' => [
'string',
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder {
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid(),
function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder {
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid()->validate($attribute, $value, $fail);
},
],
// Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z)
'start' => [
Expand Down
Loading
Loading