Compare commits

..

13 Commits

Author SHA1 Message Date
Constantin Graf
b5c0da4c6a Add postgres 18 support 2026-09-24 11:32:35 +02:00
Constantin Graf
fa0bbc8cfd Add permission config to npm-format-check GitHub action 2026-09-24 11:31:38 +02:00
Constantin Graf
fff3502e53 Updated UUID generation 2026-09-24 11:31:38 +02:00
Constantin Graf
e801c4311c Add new extension auditing 2026-09-24 11:31:38 +02:00
Constantin Graf
2dadf1ef02 Updated Laravel to v13 2026-09-24 10:45:18 +02:00
Constantin Graf
12cc3f27f8 Add check for premium to telemetry and update lookup 2026-09-24 10:45:18 +02:00
Constantin Graf
06a2048771 Add basic e2e tests for admin panel 2026-09-24 10:45:18 +02:00
Constantin Graf
a12ee1b029 Address review feedback on composer updates
- Default INERTIA_SSR_ENABLED to false to match .env.example, .env.ci and
  .env.production, so existing setups keep the previous behaviour.
- Stop tracking the published Filament assets under /public/fonts/filament and
  add them to .gitignore, as recommended by the Filament docs. They are
  regenerated by filament:assets via the filament:upgrade post-autoload-dump
  hook. /public/css and /public/js are already ignored.
- Drop the now dead theme.css filter from the Vite asset list in app.blade.php,
  since the custom Filament theme was removed along with viteTheme().
2026-09-24 10:45:18 +02:00
Constantin Graf
54dc0dbabc Add psysh to gitignore 2026-09-24 10:45:18 +02:00
Constantin Graf
08f9edbc96 Add additional tests for filament 2026-09-24 10:45:18 +02:00
Constantin Graf
0551c633a3 Add github action to prevent non-tag refs in manifest.json 2026-09-24 10:45:18 +02:00
Constantin Graf
37579b1e5c Updated compose dependencies 2026-09-24 10:45:18 +02:00
Constantin Graf
b33cf00788 Remove Group from ClockifyTimeEntriesImporter 2026-09-22 18:08:49 +02:00
139 changed files with 4978 additions and 2411 deletions

View File

@@ -6,6 +6,7 @@ APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
APP_FORCE_HTTPS=false APP_FORCE_HTTPS=false
APP_ENABLE_REGISTRATION=true APP_ENABLE_REGISTRATION=true
SUPER_ADMINS=admin@example.com
# Logging # Logging
LOG_CHANNEL=stack LOG_CHANNEL=stack
@@ -64,3 +65,6 @@ GOTENBERG_URL=http://localhost:3000
# Octane # Octane
OCTANE_SERVER=frankenphp OCTANE_SERVER=frankenphp
# Inertia
INERTIA_SSR_ENABLED=false

View File

@@ -88,3 +88,6 @@ FORWARD_DB_PORT=54329
VITE_HOST_NAME=vite.solidtime.test VITE_HOST_NAME=vite.solidtime.test
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
#SAIL_XDEBUG_MODE=develop,debug,coverage #SAIL_XDEBUG_MODE=develop,debug,coverage
# This app does not ship a Node SSR bundle, so Inertia SSR must stay disabled.
INERTIA_SSR_ENABLED=false

View File

@@ -13,3 +13,5 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=pgsql DB_CONNECTION=pgsql
INERTIA_SSR_ENABLED=false

View File

@@ -103,6 +103,8 @@ jobs:
{ {
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)" echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)" echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: "Checkout invoicing extension" - name: "Checkout invoicing extension"
@@ -119,9 +121,26 @@ jobs:
- name: "Install npm dependencies in invoicing extension" - name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci run: cd extensions/Invoicing && npm ci
- name: "Checkout auditing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
path: extensions/Auditing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
- name: "Install composer dependencies in auditing extension"
run: cd extensions/Auditing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
- name: "Install npm dependencies in auditing extension"
run: cd extensions/Auditing && npm ci
- name: "Activate invoicing extension" - name: "Activate invoicing extension"
run: php artisan module:enable Invoicing run: php artisan module:enable Invoicing
- name: "Activate auditing extension"
run: php artisan module:enable Auditing
- name: "Install npm dependencies" - name: "Install npm dependencies"
run: npm ci run: npm ci

View File

@@ -84,6 +84,8 @@ jobs:
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)" echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)" echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)" echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: "Checkout billing extension" - name: "Checkout billing extension"
@@ -145,6 +147,25 @@ jobs:
- name: "Install npm dependencies in invoicing extension" - name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci run: cd extensions/Invoicing && npm ci
- name: "Checkout auditing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
path: extensions/Auditing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
- name: "Install composer dependencies in auditing extension"
uses: php-actions/composer@v6
with:
working_dir: "extensions/Auditing"
command: install
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
php_version: 8.3
- name: "Install npm dependencies in auditing extension"
run: cd extensions/Auditing && npm ci
- name: "Setup PHP with PECL extension" - name: "Setup PHP with PECL extension"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
@@ -168,6 +189,9 @@ jobs:
- name: "Activate invoicing extension" - name: "Activate invoicing extension"
run: php artisan module:enable Invoicing run: php artisan module:enable Invoicing
- name: "Activate auditing extension"
run: php artisan module:enable Auditing
- name: "Install npm dependencies" - name: "Install npm dependencies"
run: npm ci run: npm ci

View File

@@ -0,0 +1,31 @@
name: Extension Manifest Check
on:
pull_request:
paths:
- extensions/manifest.json
- .github/workflows/extension-manifest-check.yml
permissions:
contents: read
jobs:
check-refs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: "Checkout code"
uses: actions/checkout@v7
# Extension refs must be pinned to a release tag (vX.Y.Z) or a full commit
# SHA. Branch names (main, feature/foo, ...) are only acceptable while
# developing and must be replaced before the pull request is merged.
- name: "Check extension refs are pinned"
run: |
failed=0
while IFS=$'\t' read -r name ref; do
if [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.]+)?$ ]] || [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
echo "✅ $name: $ref"
else
echo "::error file=extensions/manifest.json::Extension \"$name\" uses ref \"$ref\", which is not a release tag or commit SHA. Pin it before merging."
failed=1
fi
done < <(jq -r 'to_entries[] | [.key, .value.ref] | @tsv' extensions/manifest.json)
exit $failed

View File

@@ -1,6 +1,8 @@
name: NPM Format Check name: NPM Format Check
on: [push] on: [push]
permissions:
contents: read
jobs: jobs:
format-check: format-check:

View File

@@ -8,7 +8,7 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
strategy: strategy:
matrix: matrix:
postgres_version: [ 15, 16, 17 ] postgres_version: [ 15, 16, 17, 18 ]
services: services:
pgsql_test: pgsql_test:

View File

@@ -8,7 +8,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
strategy: strategy:
matrix: matrix:
postgres_version: [ 15, 16, 17 ] postgres_version: [ 15, 16, 17, 18 ]
services: services:
pgsql_test: pgsql_test:

2
.gitignore vendored
View File

@@ -6,6 +6,7 @@ dist
/public/storage /public/storage
/public/css /public/css
/public/js /public/js
/public/fonts/filament
/public/vendor /public/vendor
/lang/vendor /lang/vendor
/storage/*.key /storage/*.key
@@ -43,4 +44,5 @@ yarn-error.log
/data /data
/config/caddy /config/caddy
/config/composer /config/composer
/config/psysh
/AGENTS.md /AGENTS.md

View File

@@ -7,6 +7,7 @@ namespace App\Auth;
use Illuminate\Auth\EloquentUserProvider; use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Override;
/** /**
* User provider that only resolves non-placeholder users. * User provider that only resolves non-placeholder users.
@@ -26,7 +27,7 @@ class ActiveUserProvider extends EloquentUserProvider
* @param Model|null $model * @param Model|null $model
* @return Builder<Model> * @return Builder<Model>
*/ */
#[\Override] #[Override]
protected function newModelQuery($model = null): Builder protected function newModelQuery($model = null): Builder
{ {
$query = parent::newModelQuery($model); $query = parent::newModelQuery($model);

View File

@@ -10,6 +10,7 @@ use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use stdClass;
class SelfHostDatabaseConsistency extends Command class SelfHostDatabaseConsistency extends Command
{ {
@@ -101,7 +102,7 @@ class SelfHostDatabaseConsistency extends Command
} }
/** /**
* @param Collection<int, \stdClass> $problems * @param Collection<int, stdClass> $problems
*/ */
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
{ {

View File

@@ -7,7 +7,7 @@ namespace App\Console\Commands\SelfHost;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter; use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use phpseclib3\Crypt\RSA; use phpseclib4\Crypt\RSA;
class SelfHostGenerateKeysCommand extends Command class SelfHostGenerateKeysCommand extends Command
{ {

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Exceptions\Api; namespace App\Exceptions\Api;
use Override;
class EntityStillInUseApiException extends ApiException class EntityStillInUseApiException extends ApiException
{ {
private string $modelToDelete; private string $modelToDelete;
@@ -32,7 +34,7 @@ class EntityStillInUseApiException extends ApiException
/** /**
* Get the translated message for the exception. * Get the translated message for the exception.
*/ */
#[\Override] #[Override]
public function getTranslatedMessage(): string public function getTranslatedMessage(): string
{ {
return __('exceptions.api.'.$this->getKey(), [ return __('exceptions.api.'.$this->getKey(), [

View File

@@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\AuditResource\Pages;
use App\Models\Audit;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
class AuditResource extends Resource
{
protected static ?string $model = Audit::class;
protected static ?string $navigationIcon = 'heroicon-o-archive-box';
protected static ?string $navigationGroup = 'System';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('user_type')
->maxLength(255),
Forms\Components\TextInput::make('user_id'),
Forms\Components\TextInput::make('event')
->required()
->maxLength(255),
Forms\Components\TextInput::make('auditable_type')
->required()
->maxLength(255),
Forms\Components\TextInput::make('auditable_id')
->required(),
PrettyJsonField::make('old_values'),
PrettyJsonField::make('new_values'),
Forms\Components\Textarea::make('url'),
Forms\Components\TextInput::make('ip_address'),
Forms\Components\TextInput::make('user_agent')
->maxLength(1023),
Forms\Components\TextInput::make('tags')
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('event'),
Tables\Columns\TextColumn::make('auditable_type'),
Tables\Columns\TextColumn::make('auditable_id'),
IconColumn::make('was_command')
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->sortable()
->dateTime(),
Tables\Columns\TextColumn::make('updated_at')
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\ViewAction::make(),
])
->bulkActions([
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListAudits::route('/'),
'create' => Pages\CreateAudit::route('/create'),
'view' => Pages\ViewAudit::route('/{record}'),
];
}
}

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\CreateRecord;
class CreateAudit extends CreateRecord
{
protected static string $resource = AuditResource::class;
}

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ListRecords;
class ListAudits extends ListRecords
{
protected static string $resource = AuditResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ViewRecord;
class ViewAudit extends ViewRecord
{
protected static string $resource = AuditResource::class;
}

View File

@@ -4,13 +4,18 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\ClientResource\Pages; use App\Filament\Resources\ClientResource\Pages\CreateClient;
use App\Filament\Resources\ClientResource\Pages\EditClient;
use App\Filament\Resources\ClientResource\Pages\ListClients;
use App\Models\Client; use App\Models\Client;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -18,16 +23,16 @@ class ClientResource extends Resource
{ {
protected static ?string $model = Client::class; protected static ?string $model = Client::class;
protected static ?string $navigationIcon = 'heroicon-o-briefcase'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-briefcase';
protected static ?string $navigationGroup = 'Timetracking'; protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 4; protected static ?int $navigationSort = 4;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required(), ->required(),
@@ -43,17 +48,17 @@ class ClientResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name') TextColumn::make('name')
->label('Name') ->label('Name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('organization.name') TextColumn::make('organization.name')
->sortable() ->sortable()
->label('Organization'), ->label('Organization'),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->label('Created at') ->label('Created at')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->label('Updated at') ->label('Updated at')
->sortable(), ->sortable(),
]) ])
@@ -68,12 +73,12 @@ class ClientResource extends Resource
->relationship('organization', 'id') ->relationship('organization', 'id')
->searchable(), ->searchable(),
]) ])
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -88,9 +93,9 @@ class ClientResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListClients::route('/'), 'index' => ListClients::route('/'),
'create' => Pages\CreateClient::route('/create'), 'create' => CreateClient::route('/create'),
'edit' => Pages\EditClient::route('/{record}/edit'), 'edit' => EditClient::route('/{record}/edit'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages; namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource; use App\Filament\Resources\ClientResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditClient extends EditRecord class EditClient extends EditRecord
@@ -15,7 +15,7 @@ class EditClient extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages; namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource; use App\Filament\Resources\ClientResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListClients extends ListRecords class ListClients extends ListRecords
@@ -15,7 +15,7 @@ class ListClients extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -7,16 +7,16 @@ namespace App\Filament\Resources;
use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs; use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs;
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs; use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
use App\Models\FailedJob; use App\Models\FailedJob;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\Action; use Filament\Schemas\Schema;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -30,19 +30,19 @@ class FailedJobResource extends Resource
{ {
protected static ?string $model = FailedJob::class; protected static ?string $model = FailedJob::class;
protected static ?string $navigationIcon = 'heroicon-o-exclamation-circle'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-circle';
protected static ?string $navigationGroup = 'System'; protected static string|\UnitEnum|null $navigationGroup = 'System';
public static function getNavigationBadge(): ?string public static function getNavigationBadge(): ?string
{ {
return (string) FailedJob::query()->count(); return (string) FailedJob::query()->count();
} }
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
TextInput::make('uuid')->disabled()->columnSpan(4), TextInput::make('uuid')->disabled()->columnSpan(4),
TextInput::make('failed_at')->disabled(), TextInput::make('failed_at')->disabled(),
TextInput::make('id')->disabled(), TextInput::make('id')->disabled(),
@@ -74,7 +74,7 @@ class FailedJobResource extends Resource
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true), TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([]) ->filters([])
->bulkActions([ ->toolbarActions([
BulkAction::make('retry') BulkAction::make('retry')
->icon('heroicon-o-arrow-path') ->icon('heroicon-o-arrow-path')
->label('Retry selected') ->label('Retry selected')
@@ -91,7 +91,7 @@ class FailedJobResource extends Resource
}), }),
DeleteBulkAction::make(), DeleteBulkAction::make(),
]) ])
->actions([ ->recordActions([
DeleteAction::make(), DeleteAction::make(),
ViewAction::make(), ViewAction::make(),
Action::make('retry') Action::make('retry')

View File

@@ -5,14 +5,21 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Enums\Role; use App\Enums\Role;
use App\Filament\Resources\OrganizationInvitationResource\Pages; use App\Filament\Resources\OrganizationInvitationResource\Pages\EditOrganizationInvitation;
use App\Filament\Resources\OrganizationInvitationResource\Pages\ListOrganizationInvitations;
use App\Filament\Resources\OrganizationInvitationResource\Pages\ViewOrganizationInvitation;
use App\Models\OrganizationInvitation; use App\Models\OrganizationInvitation;
use App\Service\OrganizationInvitationService; use App\Service\OrganizationInvitationService;
use Filament\Forms; use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Form; use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -22,18 +29,18 @@ class OrganizationInvitationResource extends Resource
protected static ?string $label = 'Invitations'; protected static ?string $label = 'Invitations';
protected static ?string $navigationIcon = 'heroicon-o-user-plus'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user-plus';
protected static ?string $navigationGroup = 'Users'; protected static string|\UnitEnum|null $navigationGroup = 'Users';
protected static ?int $navigationSort = 9; protected static ?int $navigationSort = 9;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->columns(1) ->columns(1)
->schema([ ->components([
Forms\Components\TextInput::make('email') TextInput::make('email')
->label('Email') ->label('Email')
->disabledOn(['edit']) ->disabledOn(['edit'])
->required(), ->required(),
@@ -45,11 +52,11 @@ class OrganizationInvitationResource extends Resource
->searchable(['name']) ->searchable(['name'])
->disabledOn(['edit']) ->disabledOn(['edit'])
->required(), ->required(),
Forms\Components\DateTimePicker::make('created_at') DateTimePicker::make('created_at')
->label('Created At') ->label('Created At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
Forms\Components\DateTimePicker::make('updated_at') DateTimePicker::make('updated_at')
->label('Updated At') ->label('Updated At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
@@ -60,17 +67,17 @@ class OrganizationInvitationResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('organization.name') TextColumn::make('organization.name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('email') TextColumn::make('email')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('role'), TextColumn::make('role'),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->label('Created At') ->label('Created At')
->dateTime() ->dateTime()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->label('Updated At') ->label('Updated At')
->dateTime() ->dateTime()
->sortable() ->sortable()
@@ -80,13 +87,13 @@ class OrganizationInvitationResource extends Resource
->filters([ ->filters([
// //
]) ])
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
Tables\Actions\DeleteAction::make(), DeleteAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\BulkAction::make('resend') BulkAction::make('resend')
->label('Resend') ->label('Resend')
->action(function (Collection $records): void { ->action(function (Collection $records): void {
foreach ($records as $organizationInvite) { foreach ($records as $organizationInvite) {
@@ -106,9 +113,9 @@ class OrganizationInvitationResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListOrganizationInvitations::route('/'), 'index' => ListOrganizationInvitations::route('/'),
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'), 'edit' => EditOrganizationInvitation::route('/{record}/edit'),
'view' => Pages\ViewOrganizationInvitation::route('/{record}'), 'view' => ViewOrganizationInvitation::route('/{record}'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationInvitationResource\Pages; namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
use App\Filament\Resources\OrganizationInvitationResource; use App\Filament\Resources\OrganizationInvitationResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditOrganizationInvitation extends EditRecord class EditOrganizationInvitation extends EditRecord
@@ -15,7 +15,7 @@ class EditOrganizationInvitation extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -9,7 +9,10 @@ use App\Enums\DateFormat;
use App\Enums\IntervalFormat; use App\Enums\IntervalFormat;
use App\Enums\NumberFormat; use App\Enums\NumberFormat;
use App\Enums\TimeFormat; use App\Enums\TimeFormat;
use App\Filament\Resources\OrganizationResource\Pages; use App\Filament\Resources\OrganizationResource\Pages\CreateOrganization;
use App\Filament\Resources\OrganizationResource\Pages\EditOrganization;
use App\Filament\Resources\OrganizationResource\Pages\ListOrganizations;
use App\Filament\Resources\OrganizationResource\Pages\ViewOrganization;
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager; use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager; use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
use App\Models\Organization; use App\Models\Organization;
@@ -20,38 +23,45 @@ use App\Service\Import\Importers\ImportException;
use App\Service\Import\Importers\ReportDto; use App\Service\Import\Importers\ReportDto;
use App\Service\Import\ImportService; use App\Service\Import\ImportService;
use App\Service\TimezoneService; use App\Service\TimezoneService;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\IsoCurrencyProvider;
use Filament\Forms; use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Form; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action; use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToReadFile;
class OrganizationResource extends Resource class OrganizationResource extends Resource
{ {
protected static ?string $model = Organization::class; protected static ?string $model = Organization::class;
protected static ?string $navigationIcon = 'heroicon-o-building-office-2'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
protected static ?string $navigationGroup = 'Users'; protected static string|\UnitEnum|null $navigationGroup = 'Users';
protected static ?int $navigationSort = 7; protected static ?int $navigationSort = 7;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->columns(1) ->columns(1)
->schema([ ->components([
Forms\Components\TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required() ->required()
->maxLength(255), ->maxLength(255),
Forms\Components\Toggle::make('personal_team') Toggle::make('personal_team')
->label('Is personal?') ->label('Is personal?')
->hiddenOn(['create']) ->hiddenOn(['create'])
->required(), ->required(),
@@ -79,7 +89,7 @@ class OrganizationResource extends Resource
Select::make('currency') Select::make('currency')
->label('Currency') ->label('Currency')
->options(function (): array { ->options(function (): array {
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies(); $currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
$select = []; $select = [];
foreach ($currencies as $currency) { foreach ($currencies as $currency) {
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')'; $select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
@@ -89,7 +99,7 @@ class OrganizationResource extends Resource
}) })
->required() ->required()
->searchable(), ->searchable(),
Forms\Components\TextInput::make('billable_rate') TextInput::make('billable_rate')
->label('Billable rate (in Cents)') ->label('Billable rate (in Cents)')
->nullable() ->nullable()
->rules([ ->rules([
@@ -99,11 +109,11 @@ class OrganizationResource extends Resource
'max:2147483647', 'max:2147483647',
]) ])
->numeric(), ->numeric(),
Forms\Components\DateTimePicker::make('created_at') DateTimePicker::make('created_at')
->label('Created At') ->label('Created At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
Forms\Components\DateTimePicker::make('updated_at') DateTimePicker::make('updated_at')
->label('Updated At') ->label('Updated At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
@@ -117,7 +127,7 @@ class OrganizationResource extends Resource
TextColumn::make('name') TextColumn::make('name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\IconColumn::make('personal_team') IconColumn::make('personal_team')
->boolean() ->boolean()
->label('Is personal?') ->label('Is personal?')
->sortable(), ->sortable(),
@@ -138,9 +148,9 @@ class OrganizationResource extends Resource
->filters([ ->filters([
// //
]) ])
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
Tables\Actions\DeleteAction::make() DeleteAction::make()
->using(function (Organization $record): void { ->using(function (Organization $record): void {
app(DeletionService::class)->deleteOrganization($record); app(DeletionService::class)->deleteOrganization($record);
}), }),
@@ -158,7 +168,7 @@ class OrganizationResource extends Resource
return response()->streamDownload(function () use ($file): void { return response()->streamDownload(function () use ($file): void {
echo Storage::disk(config('filesystems.private'))->get($file); echo Storage::disk(config('filesystems.private'))->get($file);
}, 'export.zip'); }, 'export.zip');
} catch (\Exception $exception) { } catch (Exception $exception) {
report($exception); report($exception);
Notification::make() Notification::make()
->title('Export failed') ->title('Export failed')
@@ -172,9 +182,13 @@ class OrganizationResource extends Resource
->icon('heroicon-o-inbox-arrow-down') ->icon('heroicon-o-inbox-arrow-down')
->action(function (Organization $record, array $data): void { ->action(function (Organization $record, array $data): void {
try { try {
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']); try {
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
} catch (UnableToReadFile) {
$file = null;
}
if ($file === null) { if ($file === null) {
throw new \Exception('File not found'); throw new ImportException('File not found');
} }
/** @var string $timezone */ /** @var string $timezone */
$timezone = $data['timezone']; $timezone = $data['timezone'];
@@ -209,8 +223,8 @@ class OrganizationResource extends Resource
} }
}) })
->tooltip(fn (Organization $record): string => 'Import into '.$record->name) ->tooltip(fn (Organization $record): string => 'Import into '.$record->name)
->form([ ->schema([
Forms\Components\FileUpload::make('file') FileUpload::make('file')
->label('File') ->label('File')
->required(), ->required(),
Select::make('type') Select::make('type')
@@ -230,7 +244,7 @@ class OrganizationResource extends Resource
->required(), ->required(),
]), ]),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
@@ -245,10 +259,10 @@ class OrganizationResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListOrganizations::route('/'), 'index' => ListOrganizations::route('/'),
'create' => Pages\CreateOrganization::route('/create'), 'create' => CreateOrganization::route('/create'),
'edit' => Pages\EditOrganization::route('/{record}/edit'), 'edit' => EditOrganization::route('/{record}/edit'),
'view' => Pages\ViewOrganization::route('/{record}'), 'view' => ViewOrganization::route('/{record}'),
]; ];
} }
} }

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages; namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource; use App\Filament\Resources\OrganizationResource;
use App\Filament\Resources\OrganizationResource\Actions\DeleteOrganization;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditOrganization extends EditRecord class EditOrganization extends EditRecord
@@ -14,7 +15,7 @@ class EditOrganization extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
OrganizationResource\Actions\DeleteOrganization::make(), DeleteOrganization::make(),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\OrganizationResource\Pages; namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource; use App\Filament\Resources\OrganizationResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListOrganizations extends ListRecords class ListOrganizations extends ListRecords
@@ -15,7 +15,7 @@ class ListOrganizations extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -9,12 +9,17 @@ use App\Filament\Resources\OrganizationInvitationResource;
use App\Models\Organization; use App\Models\Organization;
use App\Models\OrganizationInvitation; use App\Models\OrganizationInvitation;
use App\Service\InvitationService; use App\Service\InvitationService;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DetachBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@@ -25,10 +30,10 @@ class InvitationsRelationManager extends RelationManager
protected static ?string $title = 'Invitations'; protected static ?string $title = 'Invitations';
public function form(Form $form): Form public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
TextInput::make('email') TextInput::make('email')
->label('Email') ->label('Email')
->disabledOn(['edit']) ->disabledOn(['edit'])
@@ -53,33 +58,33 @@ class InvitationsRelationManager extends RelationManager
->modelLabel('Invitation') ->modelLabel('Invitation')
->pluralModelLabel('Invitations') ->pluralModelLabel('Invitations')
->columns([ ->columns([
Tables\Columns\TextColumn::make('email'), TextColumn::make('email'),
Tables\Columns\TextColumn::make('role'), TextColumn::make('role'),
]) ])
->headerActions([ ->headerActions([
Tables\Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus') ->icon('heroicon-s-plus')
->using(function (array $data, string $model): Model { ->using(function (array $data, string $model): Model {
/** @var Organization $ownerRecord */ /** @var Organization $ownerRecord */
$ownerRecord = $this->getOwnerRecord(); $ownerRecord = $this->getOwnerRecord();
return app(InvitationService::class) return app(InvitationService::class)
->inviteUser($ownerRecord, $data['email'], Role::from($data['role']), auth()->user()); ->inviteUser($ownerRecord, $data['email'], ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), auth()->user());
}), }),
]) ])
->actions([ ->recordActions([
Action::make('view') Action::make('view')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [ ->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
'record' => $record->getKey(), 'record' => $record->getKey(),
])), ])),
Tables\Actions\EditAction::make(), EditAction::make(),
Tables\Actions\DeleteAction::make(), DeleteAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DetachBulkAction::make(), DetachBulkAction::make(),
]), ]),
]); ]);
} }

View File

@@ -12,14 +12,15 @@ use App\Models\Organization;
use App\Models\User; use App\Models\User;
use App\Service\BillableRateService; use App\Service\BillableRateService;
use App\Service\MemberService; use App\Service\MemberService;
use Filament\Actions\Action;
use Filament\Actions\AttachAction;
use Filament\Actions\DetachAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\AttachAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@@ -28,10 +29,10 @@ class UsersRelationManager extends RelationManager
{ {
protected static string $relationship = 'users'; protected static string $relationship = 'users';
public function form(Form $form): Form public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Select::make('role') Select::make('role')
->options(Role::class), ->options(Role::class),
TextInput::make('billable_rate') TextInput::make('billable_rate')
@@ -75,17 +76,17 @@ class UsersRelationManager extends RelationManager
->using(function (User $record, array $data): void { ->using(function (User $record, array $data): void {
/** @var Organization $organization */ /** @var Organization $organization */
$organization = $this->getOwnerRecord(); $organization = $this->getOwnerRecord();
app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true); app(MemberService::class)->addMember($record, $organization, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
}), }),
]) ])
->actions([ ->recordActions([
Action::make('view') Action::make('view')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
->url(fn (User $record): string => UserResource::getUrl('view', [ ->url(fn (User $record): string => UserResource::getUrl('view', [
'record' => $record->getKey(), 'record' => $record->getKey(),
])), ])),
Tables\Actions\EditAction::make() EditAction::make()
->using(function (User $record, array $data): User { ->using(function (User $record, array $data): User {
/** @var Organization $organization */ /** @var Organization $organization */
$organization = $this->getOwnerRecord(); $organization = $this->getOwnerRecord();
@@ -97,9 +98,10 @@ class UsersRelationManager extends RelationManager
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member); app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
} }
if ($data['role'] !== $member->role) { $newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
if ($newRole->value !== $member->role) {
try { try {
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true); app(MemberService::class)->changeRole($member, $organization, $newRole, true);
} catch (ApiException $exception) { } catch (ApiException $exception) {
Notification::make() Notification::make()
->danger() ->danger()
@@ -113,7 +115,7 @@ class UsersRelationManager extends RelationManager
return $record; return $record;
}), }),
Tables\Actions\DetachAction::make() DetachAction::make()
->using(function (User $record): void { ->using(function (User $record): void {
/** @var Organization $organization */ /** @var Organization $organization */
$organization = $this->getOwnerRecord(); $organization = $this->getOwnerRecord();
@@ -133,7 +135,7 @@ class UsersRelationManager extends RelationManager
} }
}), }),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
} }

View File

@@ -4,12 +4,19 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\ProjectMemberResource\Pages; use App\Filament\Resources\ProjectMemberResource\Pages\CreateProjectMember;
use App\Filament\Resources\ProjectMemberResource\Pages\EditProjectMember;
use App\Filament\Resources\ProjectMemberResource\Pages\ListProjectMembers;
use App\Filament\Resources\ProjectMemberResource\Pages\ViewProjectMembers;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use Filament\Forms; use Filament\Actions\BulkActionGroup;
use Filament\Forms\Form; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
class ProjectMemberResource extends Resource class ProjectMemberResource extends Resource
@@ -18,11 +25,11 @@ class ProjectMemberResource extends Resource
protected static bool $shouldRegisterNavigation = false; protected static bool $shouldRegisterNavigation = false;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Forms\Components\TextInput::make('billable_rate') TextInput::make('billable_rate')
->label('Billable rate (in Cents)') ->label('Billable rate (in Cents)')
->nullable() ->nullable()
->rules([ ->rules([
@@ -32,10 +39,10 @@ class ProjectMemberResource extends Resource
'max:2147483647', 'max:2147483647',
]) ])
->numeric(), ->numeric(),
Forms\Components\Select::make('user_id') Select::make('user_id')
->relationship('user', 'name') ->relationship('user', 'name')
->required(), ->required(),
Forms\Components\Select::make('member_id') Select::make('member_id')
->relationship('member', 'id') ->relationship('member', 'id')
->required(), ->required(),
]); ]);
@@ -45,17 +52,17 @@ class ProjectMemberResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('id') TextColumn::make('id')
->label('ID'), ->label('ID'),
Tables\Columns\TextColumn::make('billable_rate') TextColumn::make('billable_rate')
->numeric() ->numeric()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('project.name'), TextColumn::make('project.name'),
Tables\Columns\TextColumn::make('user.name'), TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->dateTime() ->dateTime()
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
@@ -63,12 +70,12 @@ class ProjectMemberResource extends Resource
->filters([ ->filters([
// //
]) ])
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -83,10 +90,10 @@ class ProjectMemberResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListProjectMembers::route('/'), 'index' => ListProjectMembers::route('/'),
'create' => Pages\CreateProjectMember::route('/create'), 'create' => CreateProjectMember::route('/create'),
'edit' => Pages\EditProjectMember::route('/{record}/edit'), 'edit' => EditProjectMember::route('/{record}/edit'),
'view' => Pages\ViewProjectMembers::route('/{record}'), 'view' => ViewProjectMembers::route('/{record}'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectMemberResource\Pages; namespace App\Filament\Resources\ProjectMemberResource\Pages;
use App\Filament\Resources\ProjectMemberResource; use App\Filament\Resources\ProjectMemberResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditProjectMember extends EditRecord class EditProjectMember extends EditRecord
@@ -15,7 +15,7 @@ class EditProjectMember extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectMemberResource\Pages; namespace App\Filament\Resources\ProjectMemberResource\Pages;
use App\Filament\Resources\ProjectMemberResource; use App\Filament\Resources\ProjectMemberResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListProjectMembers extends ListRecords class ListProjectMembers extends ListRecords
@@ -15,7 +15,7 @@ class ListProjectMembers extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -4,14 +4,19 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\ProjectResource\Pages; use App\Filament\Resources\ProjectResource\Pages\CreateProject;
use App\Filament\Resources\ProjectResource\Pages\EditProject;
use App\Filament\Resources\ProjectResource\Pages\ListProjects;
use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager; use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager;
use App\Models\Project; use App\Models\Project;
use Filament\Forms; use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\ColorPicker; use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Form; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\ColorColumn; use Filament\Tables\Columns\ColorColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@@ -21,24 +26,24 @@ class ProjectResource extends Resource
{ {
protected static ?string $model = Project::class; protected static ?string $model = Project::class;
protected static ?string $navigationIcon = 'heroicon-o-folder'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-folder';
protected static ?string $navigationGroup = 'Timetracking'; protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 2;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Forms\Components\TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required() ->required()
->maxLength(255), ->maxLength(255),
ColorPicker::make('color') ColorPicker::make('color')
->label('Color') ->label('Color')
->required(), ->required(),
Forms\Components\TextInput::make('billable_rate') TextInput::make('billable_rate')
->label('Billable rate (in Cents)') ->label('Billable rate (in Cents)')
->nullable() ->nullable()
->rules([ ->rules([
@@ -48,7 +53,7 @@ class ProjectResource extends Resource
'max:2147483647', 'max:2147483647',
]) ])
->numeric(), ->numeric(),
Forms\Components\Select::make('organization_id') Select::make('organization_id')
->relationship(name: 'organization', titleAttribute: 'name') ->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name']) ->searchable(['name'])
->required(), ->required(),
@@ -81,12 +86,12 @@ class ProjectResource extends Resource
->searchable(), ->searchable(),
]) ])
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -101,9 +106,9 @@ class ProjectResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListProjects::route('/'), 'index' => ListProjects::route('/'),
'create' => Pages\CreateProject::route('/create'), 'create' => CreateProject::route('/create'),
'edit' => Pages\EditProject::route('/{record}/edit'), 'edit' => EditProject::route('/{record}/edit'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages; namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource; use App\Filament\Resources\ProjectResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditProject extends EditRecord class EditProject extends EditRecord
@@ -15,7 +15,7 @@ class EditProject extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ProjectResource\Pages; namespace App\Filament\Resources\ProjectResource\Pages;
use App\Filament\Resources\ProjectResource; use App\Filament\Resources\ProjectResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListProjects extends ListRecords class ListProjects extends ListRecords
@@ -15,7 +15,7 @@ class ListProjects extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -6,10 +6,10 @@ namespace App\Filament\Resources\ProjectResource\RelationManagers;
use App\Filament\Resources\ProjectMemberResource; use App\Filament\Resources\ProjectMemberResource;
use App\Models\ProjectMember; use App\Models\ProjectMember;
use Filament\Forms\Form; use Filament\Actions\Action;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
class ProjectMembersRelationManager extends RelationManager class ProjectMembersRelationManager extends RelationManager
@@ -18,10 +18,10 @@ class ProjectMembersRelationManager extends RelationManager
protected static string $relationship = 'members'; protected static string $relationship = 'members';
public function form(Form $form): Form public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
]); ]);
} }
@@ -30,8 +30,8 @@ class ProjectMembersRelationManager extends RelationManager
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
Tables\Columns\TextColumn::make('user.name'), TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('billable_rate') TextColumn::make('billable_rate')
->numeric() ->numeric()
->sortable(), ->sortable(),
]) ])
@@ -40,7 +40,7 @@ class ProjectMembersRelationManager extends RelationManager
]) ])
->headerActions([ ->headerActions([
]) ])
->actions([ ->recordActions([
Action::make('view') Action::make('view')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
@@ -54,7 +54,7 @@ class ProjectMembersRelationManager extends RelationManager
])) ]))
->openUrlInNewTab(), ->openUrlInNewTab(),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
} }

View File

@@ -4,16 +4,21 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\ReportResource\Pages; use App\Filament\Resources\ReportResource\Pages\EditReport;
use App\Filament\Resources\ReportResource\Pages\ListReports;
use App\Filament\Resources\ReportResource\Pages\ViewReport;
use App\Models\Report; use App\Models\Report;
use App\Service\Dto\ReportPropertiesDto; use App\Service\Dto\ReportPropertiesDto;
use Filament\Forms; use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@@ -24,22 +29,22 @@ class ReportResource extends Resource
{ {
protected static ?string $model = Report::class; protected static ?string $model = Report::class;
protected static ?string $navigationIcon = 'heroicon-o-document-chart-bar'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-chart-bar';
protected static ?string $navigationGroup = 'Timetracking'; protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 7; protected static ?int $navigationSort = 7;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->columns(1) ->columns(1)
->schema([ ->components([
Forms\Components\TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required() ->required()
->maxLength(255), ->maxLength(255),
Forms\Components\TextInput::make('description') TextInput::make('description')
->label('Description') ->label('Description')
->nullable() ->nullable()
->maxLength(255), ->maxLength(255),
@@ -49,13 +54,13 @@ class ReportResource extends Resource
DateTimePicker::make('public_until') DateTimePicker::make('public_until')
->label('Public until') ->label('Public until')
->nullable(), ->nullable(),
Forms\Components\Select::make('organization_id') Select::make('organization_id')
->label('Organization') ->label('Organization')
->relationship(name: 'organization', titleAttribute: 'name') ->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name']) ->searchable(['name'])
->disabled() ->disabled()
->required(), ->required(),
Forms\Components\TextInput::make('share_secret') TextInput::make('share_secret')
->label('Share Secret') ->label('Share Secret')
->nullable(), ->nullable(),
PrettyJsonField::make('properties') PrettyJsonField::make('properties')
@@ -109,18 +114,18 @@ class ReportResource extends Resource
->relationship('organization', 'id') ->relationship('organization', 'id')
->searchable(), ->searchable(),
]) ])
->actions([ ->recordActions([
Action::make('public-view') Action::make('public-view')
->label('Public') ->label('Public')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
->hidden(fn (Report $record): bool => $record->getShareableLink() === null) ->hidden(fn (Report $record): bool => $record->getShareableLink() === null)
->url(fn (Report $record): string => $record->getShareableLink(), true), ->url(fn (Report $record): string => $record->getShareableLink(), true),
Tables\Actions\ViewAction::make(), ViewAction::make(),
Tables\Actions\EditAction::make(), EditAction::make(),
Tables\Actions\DeleteAction::make(), DeleteAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
@@ -133,9 +138,9 @@ class ReportResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListReports::route('/'), 'index' => ListReports::route('/'),
'edit' => Pages\EditReport::route('/{record}/edit'), 'edit' => EditReport::route('/{record}/edit'),
'view' => Pages\ViewReport::route('/{record}'), 'view' => ViewReport::route('/{record}'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ReportResource\Pages; namespace App\Filament\Resources\ReportResource\Pages;
use App\Filament\Resources\ReportResource; use App\Filament\Resources\ReportResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditReport extends EditRecord class EditReport extends EditRecord
@@ -15,7 +15,7 @@ class EditReport extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -4,13 +4,18 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\TagResource\Pages; use App\Filament\Resources\TagResource\Pages\CreateTag;
use App\Filament\Resources\TagResource\Pages\EditTag;
use App\Filament\Resources\TagResource\Pages\ListTags;
use App\Models\Tag; use App\Models\Tag;
use Filament\Forms; use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -18,20 +23,20 @@ class TagResource extends Resource
{ {
protected static ?string $model = Tag::class; protected static ?string $model = Tag::class;
protected static ?string $navigationIcon = 'heroicon-o-tag'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-tag';
protected static ?string $navigationGroup = 'Timetracking'; protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 5; protected static ?int $navigationSort = 5;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required(), ->required(),
Forms\Components\Select::make('organization_id') Select::make('organization_id')
->relationship(name: 'organization', titleAttribute: 'name') ->relationship(name: 'organization', titleAttribute: 'name')
->label('Organization') ->label('Organization')
->searchable(['name']) ->searchable(['name'])
@@ -43,17 +48,17 @@ class TagResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name') TextColumn::make('name')
->label('Name') ->label('Name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('organization.name') TextColumn::make('organization.name')
->sortable() ->sortable()
->label('Organization'), ->label('Organization'),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->label('Created at') ->label('Created at')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->label('Updated at') ->label('Updated at')
->sortable(), ->sortable(),
]) ])
@@ -68,12 +73,12 @@ class TagResource extends Resource
->relationship('organization', 'id') ->relationship('organization', 'id')
->searchable(), ->searchable(),
]) ])
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -88,9 +93,9 @@ class TagResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListTags::route('/'), 'index' => ListTags::route('/'),
'create' => Pages\CreateTag::route('/create'), 'create' => CreateTag::route('/create'),
'edit' => Pages\EditTag::route('/{record}/edit'), 'edit' => EditTag::route('/{record}/edit'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages; namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource; use App\Filament\Resources\TagResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditTag extends EditRecord class EditTag extends EditRecord
@@ -15,7 +15,7 @@ class EditTag extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TagResource\Pages; namespace App\Filament\Resources\TagResource\Pages;
use App\Filament\Resources\TagResource; use App\Filament\Resources\TagResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListTags extends ListRecords class ListTags extends ListRecords
@@ -15,7 +15,7 @@ class ListTags extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -4,13 +4,18 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\TaskResource\Pages; use App\Filament\Resources\TaskResource\Pages\CreateTask;
use App\Filament\Resources\TaskResource\Pages\EditTask;
use App\Filament\Resources\TaskResource\Pages\ListTasks;
use App\Models\Task; use App\Models\Task;
use Filament\Forms; use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Form; use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -18,17 +23,17 @@ class TaskResource extends Resource
{ {
protected static ?string $model = Task::class; protected static ?string $model = Task::class;
protected static ?string $navigationIcon = 'heroicon-o-list-bullet'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-list-bullet';
protected static ?string $navigationGroup = 'Timetracking'; protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 3; protected static ?int $navigationSort = 3;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Forms\Components\TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required() ->required()
->maxLength(255), ->maxLength(255),
@@ -47,16 +52,16 @@ class TaskResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name') TextColumn::make('name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('project.name') TextColumn::make('project.name')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('organization.name') TextColumn::make('organization.name')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->sortable(), ->sortable(),
]) ])
->filters([ ->filters([
@@ -70,12 +75,12 @@ class TaskResource extends Resource
->searchable(), ->searchable(),
]) ])
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -90,9 +95,9 @@ class TaskResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListTasks::route('/'), 'index' => ListTasks::route('/'),
'create' => Pages\CreateTask::route('/create'), 'create' => CreateTask::route('/create'),
'edit' => Pages\EditTask::route('/{record}/edit'), 'edit' => EditTask::route('/{record}/edit'),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages; namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource; use App\Filament\Resources\TaskResource;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditTask extends EditRecord class EditTask extends EditRecord
@@ -15,7 +15,7 @@ class EditTask extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TaskResource\Pages; namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource; use App\Filament\Resources\TaskResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListTasks extends ListRecords class ListTasks extends ListRecords
@@ -15,7 +15,7 @@ class ListTasks extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -4,16 +4,20 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\TimeEntryResource\Pages; use App\Filament\Resources\TimeEntryResource\Pages\CreateTimeEntry;
use App\Filament\Resources\TimeEntryResource\Pages\EditTimeEntry;
use App\Filament\Resources\TimeEntryResource\Pages\ListTimeEntries;
use App\Models\Member; use App\Models\Member;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -23,16 +27,16 @@ class TimeEntryResource extends Resource
{ {
protected static ?string $model = TimeEntry::class; protected static ?string $model = TimeEntry::class;
protected static ?string $navigationIcon = 'heroicon-o-clock'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clock';
protected static ?string $navigationGroup = 'Timetracking'; protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 1; protected static ?int $navigationSort = 1;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
TextInput::make('id') TextInput::make('id')
->label('ID') ->label('ID')
->readOnly() ->readOnly()
@@ -111,12 +115,12 @@ class TimeEntryResource extends Resource
->searchable(), ->searchable(),
]) ])
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->actions([ ->recordActions([
Tables\Actions\EditAction::make(), EditAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkActionGroup::make([ BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), DeleteBulkAction::make(),
]), ]),
]); ]);
} }
@@ -131,9 +135,9 @@ class TimeEntryResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListTimeEntries::route('/'), 'index' => ListTimeEntries::route('/'),
'create' => Pages\CreateTimeEntry::route('/create'), 'create' => CreateTimeEntry::route('/create'),
'edit' => Pages\EditTimeEntry::route('/{record}/edit'), 'edit' => EditTimeEntry::route('/{record}/edit'),
]; ];
} }
} }

View File

@@ -6,7 +6,7 @@ namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource; use App\Filament\Resources\TimeEntryResource;
use App\Models\Member; use App\Models\Member;
use Filament\Actions; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditTimeEntry extends EditRecord class EditTimeEntry extends EditRecord
@@ -16,7 +16,7 @@ class EditTimeEntry extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() DeleteAction::make()
->icon('heroicon-m-trash'), ->icon('heroicon-m-trash'),
]; ];
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\TimeEntryResource\Pages; namespace App\Filament\Resources\TimeEntryResource\Pages;
use App\Filament\Resources\TimeEntryResource; use App\Filament\Resources\TimeEntryResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListTimeEntries extends ListRecords class ListTimeEntries extends ListRecords
@@ -15,7 +15,7 @@ class ListTimeEntries extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -4,12 +4,18 @@ declare(strict_types=1);
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\TokenResource\Pages; use App\Filament\Resources\TokenResource\Pages\ListTokens;
use App\Filament\Resources\TokenResource\Pages\ViewToken;
use App\Models\Passport\Token; use App\Models\Passport\Token;
use Filament\Forms; use Filament\Actions\ViewAction;
use Filament\Forms\Form; use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@@ -18,48 +24,48 @@ class TokenResource extends Resource
{ {
protected static ?string $model = Token::class; protected static ?string $model = Token::class;
protected static ?string $navigationIcon = 'heroicon-o-key'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-key';
protected static ?string $navigationGroup = 'Auth'; protected static string|\UnitEnum|null $navigationGroup = 'Auth';
protected static ?int $navigationSort = 6; protected static ?int $navigationSort = 6;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
return $form return $schema
->columns(1) ->columns(1)
->schema([ ->components([
Forms\Components\TextInput::make('id') TextInput::make('id')
->label('ID') ->label('ID')
->disabled() ->disabled()
->visibleOn(['update', 'show']) ->visibleOn(['update', 'show'])
->readOnly() ->readOnly()
->maxLength(255), ->maxLength(255),
Forms\Components\TextInput::make('name') TextInput::make('name')
->label('Name') ->label('Name')
->required() ->required()
->maxLength(255), ->maxLength(255),
Forms\Components\Select::make('owner_id') Select::make('owner_id')
->label('User') ->label('User')
->relationship(name: 'user', titleAttribute: 'name') ->relationship(name: 'user', titleAttribute: 'name')
->searchable(['name']) ->searchable(['name'])
->disabled() ->disabled()
->required(), ->required(),
Forms\Components\Select::make('client_id') Select::make('client_id')
->label('Client') ->label('Client')
->relationship(name: 'client', titleAttribute: 'name') ->relationship(name: 'client', titleAttribute: 'name')
->searchable(['name']) ->searchable(['name'])
->required(), ->required(),
Forms\Components\Toggle::make('revoked') Toggle::make('revoked')
->label('Revoked') ->label('Revoked')
->required(), ->required(),
Forms\Components\DateTimePicker::make('expires_at') DateTimePicker::make('expires_at')
->label('Expires At') ->label('Expires At')
->disabled(), ->disabled(),
Forms\Components\DateTimePicker::make('created_at') DateTimePicker::make('created_at')
->label('Created At') ->label('Created At')
->disabled(), ->disabled(),
Forms\Components\DateTimePicker::make('updated_at') DateTimePicker::make('updated_at')
->label('Updated At') ->label('Updated At')
->disabled(), ->disabled(),
]); ]);
@@ -69,32 +75,32 @@ class TokenResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name') TextColumn::make('name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('user.name') TextColumn::make('user.name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('client.name') TextColumn::make('client.name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\IconColumn::make('personal_access_client') IconColumn::make('personal_access_client')
->state(function (Token $token): bool { ->state(function (Token $token): bool {
return in_array('personal_access', $token->client->grant_types ?? [], true); return in_array('personal_access', $token->client->grant_types ?? [], true);
}) })
->boolean() ->boolean()
->label('API token?'), ->label('API token?'),
Tables\Columns\IconColumn::make('revoked') IconColumn::make('revoked')
->boolean() ->boolean()
->label('Revoked?') ->label('Revoked?')
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('expires_at') TextColumn::make('expires_at')
->dateTime() ->dateTime()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->dateTime() ->dateTime()
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
@@ -120,10 +126,10 @@ class TokenResource extends Resource
TernaryFilter::make('revoked') TernaryFilter::make('revoked')
->label('Revoked?'), ->label('Revoked?'),
]) ])
->actions([ ->recordActions([
Tables\Actions\ViewAction::make(), ViewAction::make(),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
@@ -136,8 +142,8 @@ class TokenResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListTokens::route('/'), 'index' => ListTokens::route('/'),
'view' => Pages\ViewToken::route('/{record}'), 'view' => ViewToken::route('/{record}'),
]; ];
} }
} }

View File

@@ -6,21 +6,30 @@ namespace App\Filament\Resources;
use App\Enums\Weekday; use App\Enums\Weekday;
use App\Exceptions\Api\ApiException; use App\Exceptions\Api\ApiException;
use App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource\Pages\CreateUser;
use App\Filament\Resources\UserResource\Pages\EditUser;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Filament\Resources\UserResource\Pages\ViewUser;
use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager; use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager;
use App\Filament\Resources\UserResource\RelationManagers\OwnedOrganizationsRelationManager; use App\Filament\Resources\UserResource\RelationManagers\OwnedOrganizationsRelationManager;
use App\Models\User; use App\Models\User;
use App\Service\DeletionService; use App\Service\DeletionService;
use App\Service\TimezoneService; use App\Service\TimezoneService;
use App\Service\UserService; use App\Service\UserService;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\IsoCurrencyProvider;
use Exception; use Exception;
use Filament\Forms; use Filament\Actions\BulkAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@@ -28,26 +37,26 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent; use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use STS\FilamentImpersonate\Tables\Actions\Impersonate; use STS\FilamentImpersonate\Actions\Impersonate;
class UserResource extends Resource class UserResource extends Resource
{ {
protected static ?string $model = User::class; protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-user'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user';
protected static ?string $navigationGroup = 'Users'; protected static string|\UnitEnum|null $navigationGroup = 'Users';
protected static ?int $navigationSort = 6; protected static ?int $navigationSort = 6;
public static function form(Form $form): Form public static function form(Schema $schema): Schema
{ {
/** @var User|null $record */ /** @var User|null $record */
$record = $form->getRecord(); $record = $schema->getRecord();
return $form return $schema
->columns(1) ->columns(1)
->schema([ ->components([
TextInput::make('id') TextInput::make('id')
->label('ID') ->label('ID')
->disabled() ->disabled()
@@ -69,23 +78,23 @@ class UserResource extends Resource
'email:rfc,strict', 'email:rfc,strict',
]) ])
->maxLength(255), ->maxLength(255),
Forms\Components\Toggle::make('is_placeholder') Toggle::make('is_placeholder')
->label('Is Placeholder?') ->label('Is Placeholder?')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabledOn(['edit']), ->disabledOn(['edit']),
Forms\Components\DateTimePicker::make('email_verified_at') DateTimePicker::make('email_verified_at')
->label('Email Verified At') ->label('Email Verified At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->nullable(), ->nullable(),
Forms\Components\Toggle::make('is_email_verified') Toggle::make('is_email_verified')
->label('Email Verified?') ->label('Email Verified?')
->visibleOn(['create']), ->visibleOn(['create']),
Forms\Components\Select::make('timezone') Select::make('timezone')
->label('Timezone') ->label('Timezone')
->options(fn (): array => app(TimezoneService::class)->getSelectOptions()) ->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
->searchable() ->searchable()
->required(), ->required(),
Forms\Components\Select::make('week_start') Select::make('week_start')
->label('Week Start') ->label('Week Start')
->options(Weekday::class) ->options(Weekday::class)
->required(), ->required(),
@@ -103,10 +112,10 @@ class UserResource extends Resource
->visibleOn(['create']) ->visibleOn(['create'])
->required(fn (string $context): bool => $context === 'create') ->required(fn (string $context): bool => $context === 'create')
->maxLength(255), ->maxLength(255),
Forms\Components\Select::make('currency') Select::make('currency')
->label('Currency (Personal Organization)') ->label('Currency (Personal Organization)')
->options(function (): array { ->options(function (): array {
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies(); $currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
$select = []; $select = [];
foreach ($currencies as $currency) { foreach ($currencies as $currency) {
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')'; $select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
@@ -117,11 +126,11 @@ class UserResource extends Resource
->required() ->required()
->visibleOn(['create']) ->visibleOn(['create'])
->searchable(), ->searchable(),
Forms\Components\DateTimePicker::make('created_at') DateTimePicker::make('created_at')
->label('Created At') ->label('Created At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
Forms\Components\DateTimePicker::make('updated_at') DateTimePicker::make('updated_at')
->label('Updated At') ->label('Updated At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
@@ -132,25 +141,25 @@ class UserResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
Tables\Columns\TextColumn::make('name') TextColumn::make('name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('email') TextColumn::make('email')
->icon('heroicon-m-envelope') ->icon('heroicon-m-envelope')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\IconColumn::make('is_real_user') IconColumn::make('is_real_user')
->getStateUsing(fn (User $record): bool => ! $record->is_placeholder) ->getStateUsing(fn (User $record): bool => ! $record->is_placeholder)
->label('Real user?') ->label('Real user?')
->boolean(), ->boolean(),
Tables\Columns\IconColumn::make('email_verified') IconColumn::make('email_verified')
->getStateUsing(fn (User $record): bool => $record->email_verified_at !== null) ->getStateUsing(fn (User $record): bool => $record->email_verified_at !== null)
->label('Email verified?') ->label('Email verified?')
->boolean(), ->boolean(),
Tables\Columns\TextColumn::make('created_at') TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('updated_at') TextColumn::make('updated_at')
->dateTime() ->dateTime()
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
@@ -178,7 +187,7 @@ class UserResource extends Resource
->attribute('email_verified_at') ->attribute('email_verified_at')
->nullable(), ->nullable(),
]) ])
->actions([ ->recordActions([
Impersonate::make()->before(function (User $record): void { Impersonate::make()->before(function (User $record): void {
if ($record->currentOrganization === null) { if ($record->currentOrganization === null) {
$organization = $record->organizations()->where('personal_team', '=', true)->first(); $organization = $record->organizations()->where('personal_team', '=', true)->first();
@@ -191,8 +200,8 @@ class UserResource extends Resource
app(UserService::class)->switchCurrentOrganization($record, $organization); app(UserService::class)->switchCurrentOrganization($record, $organization);
} }
}), }),
Tables\Actions\EditAction::make(), EditAction::make(),
Tables\Actions\DeleteAction::make() DeleteAction::make()
->hidden(fn (User $record) => $record->is(Auth::user())) ->hidden(fn (User $record) => $record->is(Auth::user()))
->using(function (User $record): void { ->using(function (User $record): void {
try { try {
@@ -207,8 +216,8 @@ class UserResource extends Resource
} }
}), }),
]) ])
->bulkActions([ ->toolbarActions([
Tables\Actions\BulkAction::make('Resend verification email') BulkAction::make('Resend verification email')
->icon('heroicon-o-paper-airplane') ->icon('heroicon-o-paper-airplane')
->action(function (Collection $records): void { ->action(function (Collection $records): void {
foreach ($records as $user) { foreach ($records as $user) {
@@ -230,10 +239,10 @@ class UserResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => Pages\ListUsers::route('/'), 'index' => ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'), 'create' => CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'), 'edit' => EditUser::route('/{record}/edit'),
'view' => Pages\ViewUser::route('/{record}'), 'view' => ViewUser::route('/{record}'),
]; ];
} }
} }

View File

@@ -22,7 +22,7 @@ class CreateUser extends CreateRecord
$data['email'], $data['email'],
$data['password_create'], $data['password_create'],
$data['timezone'], $data['timezone'],
Weekday::from($data['week_start']), $data['week_start'] instanceof Weekday ? $data['week_start'] : Weekday::from($data['week_start']),
$data['currency'], $data['currency'],
verifyEmail: (bool) $data['is_email_verified'] verifyEmail: (bool) $data['is_email_verified']
); );

View File

@@ -5,8 +5,9 @@ declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages; namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource; use App\Filament\Resources\UserResource;
use App\Filament\Resources\UserResource\Actions\DeleteUser;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use STS\FilamentImpersonate\Pages\Actions\Impersonate; use STS\FilamentImpersonate\Actions\Impersonate;
class EditUser extends EditRecord class EditUser extends EditRecord
{ {
@@ -16,7 +17,7 @@ class EditUser extends EditRecord
{ {
return [ return [
Impersonate::make()->record($this->getRecord()), Impersonate::make()->record($this->getRecord()),
UserResource\Actions\DeleteUser::make(), DeleteUser::make(),
]; ];
} }
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages; namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource; use App\Filament\Resources\UserResource;
use Filament\Actions; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords class ListUsers extends ListRecords
@@ -15,7 +15,7 @@ class ListUsers extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -7,7 +7,7 @@ namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource; use App\Filament\Resources\UserResource;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
use STS\FilamentImpersonate\Pages\Actions\Impersonate; use STS\FilamentImpersonate\Actions\Impersonate;
class ViewUser extends ViewRecord class ViewUser extends ViewRecord
{ {

View File

@@ -11,12 +11,13 @@ use App\Models\Member;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use App\Service\MemberService; use App\Service\MemberService;
use Filament\Actions\Action;
use Filament\Actions\DetachAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -24,10 +25,10 @@ class OrganizationsRelationManager extends RelationManager
{ {
protected static string $relationship = 'organizations'; protected static string $relationship = 'organizations';
public function form(Form $form): Form public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Select::make('role') Select::make('role')
->options(Role::class), ->options(Role::class),
]); ]);
@@ -46,21 +47,22 @@ class OrganizationsRelationManager extends RelationManager
]) ])
->headerActions([ ->headerActions([
]) ])
->actions([ ->recordActions([
Action::make('view') Action::make('view')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [ ->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
'record' => $record->getKey(), 'record' => $record->getKey(),
])), ])),
Tables\Actions\EditAction::make() EditAction::make()
->using(function (Organization $record, array $data): Organization { ->using(function (Organization $record, array $data): Organization {
/** @var Member $member */ /** @var Member $member */
$member = $record->getRelation('membership'); $member = $record->getRelation('membership');
if ($data['role'] !== $member->role) { $newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
if ($newRole->value !== $member->role) {
try { try {
app(MemberService::class)->changeRole($member, $record, Role::from($data['role']), true); app(MemberService::class)->changeRole($member, $record, $newRole, true);
} catch (ApiException $exception) { } catch (ApiException $exception) {
Notification::make() Notification::make()
->danger() ->danger()
@@ -74,7 +76,7 @@ class OrganizationsRelationManager extends RelationManager
return $record; return $record;
}), }),
Tables\Actions\DetachAction::make() DetachAction::make()
->using(function (Organization $record): void { ->using(function (Organization $record): void {
/** @var User $user */ /** @var User $user */
$user = $this->getOwnerRecord(); $user = $this->getOwnerRecord();
@@ -94,7 +96,7 @@ class OrganizationsRelationManager extends RelationManager
} }
}), }),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
} }

View File

@@ -6,10 +6,10 @@ namespace App\Filament\Resources\UserResource\RelationManagers;
use App\Filament\Resources\OrganizationResource; use App\Filament\Resources\OrganizationResource;
use App\Models\Organization; use App\Models\Organization;
use Filament\Forms\Form; use Filament\Actions\Action;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables; use Filament\Schemas\Schema;
use Filament\Tables\Actions\Action; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
class OwnedOrganizationsRelationManager extends RelationManager class OwnedOrganizationsRelationManager extends RelationManager
@@ -18,10 +18,10 @@ class OwnedOrganizationsRelationManager extends RelationManager
protected static string $relationship = 'ownedOrganizations'; protected static string $relationship = 'ownedOrganizations';
public function form(Form $form): Form public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
]); ]);
} }
@@ -30,14 +30,14 @@ class OwnedOrganizationsRelationManager extends RelationManager
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
Tables\Columns\TextColumn::make('name'), TextColumn::make('name'),
]) ])
->filters([ ->filters([
// //
]) ])
->headerActions([ ->headerActions([
]) ])
->actions([ ->recordActions([
Action::make('view') Action::make('view')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
@@ -51,7 +51,7 @@ class OwnedOrganizationsRelationManager extends RelationManager
])) ]))
->openUrlInNewTab(), ->openUrlInNewTab(),
]) ])
->bulkActions([ ->toolbarActions([
]); ]);
} }
} }

View File

@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Cache;
class ServerOverview extends Widget class ServerOverview extends Widget
{ {
protected static string $view = 'filament.widgets.server-overview'; protected string $view = 'filament.widgets.server-overview';
/** /**
* @return array<string, mixed> * @return array<string, mixed>

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class TimeEntriesCreated extends ChartWidget class TimeEntriesCreated extends ChartWidget
{ {
protected static ?string $heading = 'Time Entries Created'; protected ?string $heading = 'Time Entries Created';
public ?string $filter = 'week'; public ?string $filter = 'week';
@@ -53,7 +53,7 @@ class TimeEntriesCreated extends ChartWidget
return [ return [
'datasets' => [ 'datasets' => [
[ [
'label' => self::$heading, 'label' => $this->heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate), 'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
], ],
], ],

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class TimeEntriesImported extends ChartWidget class TimeEntriesImported extends ChartWidget
{ {
protected static ?string $heading = 'Time Entries Imported'; protected ?string $heading = 'Time Entries Imported';
public ?string $filter = 'week'; public ?string $filter = 'week';
@@ -53,7 +53,7 @@ class TimeEntriesImported extends ChartWidget
return [ return [
'datasets' => [ 'datasets' => [
[ [
'label' => self::$heading, 'label' => $this->heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate), 'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
], ],
], ],

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class UserRegistrations extends ChartWidget class UserRegistrations extends ChartWidget
{ {
protected static ?string $heading = 'User Registrations'; protected ?string $heading = 'User Registrations';
public ?string $filter = 'week'; public ?string $filter = 'week';
@@ -54,7 +54,7 @@ class UserRegistrations extends ChartWidget
return [ return [
'datasets' => [ 'datasets' => [
[ [
'label' => self::$heading, 'label' => $this->heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate), 'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
], ],
], ],

View File

@@ -14,6 +14,7 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException;
class ApiTokenController extends Controller class ApiTokenController extends Controller
{ {
@@ -62,7 +63,7 @@ class ApiTokenController extends Controller
$tokenModel = $token->getToken(); $tokenModel = $token->getToken();
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken); return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
} catch (\RuntimeException $exception) { } catch (RuntimeException $exception) {
report($exception); report($exception);
if (Str::contains($exception->getMessage(), ['Personal access client not found'])) { if (Str::contains($exception->getMessage(), ['Personal access client not found'])) {
throw new PersonalAccessClientIsNotConfiguredException; throw new PersonalAccessClientIsNotConfiguredException;

View File

@@ -7,7 +7,7 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Service\CurrencyService; use App\Service\CurrencyService;
use Brick\Money\Currency; use Brick\Money\Currency;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\IsoCurrencyProvider;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class CurrencyController extends Controller class CurrencyController extends Controller
@@ -29,7 +29,7 @@ class CurrencyController extends Controller
'name' => $currency->getName(), 'name' => $currency->getName(),
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()), 'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
], ],
ISOCurrencyProvider::getInstance()->getAvailableCurrencies() IsoCurrencyProvider::getInstance()->getAvailableCurrencies()
)); ));
return response()->json($currencies); return response()->json($currencies);

View File

@@ -33,6 +33,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable;
class MemberController extends Controller class MemberController extends Controller
{ {
@@ -146,7 +147,7 @@ class MemberController extends Controller
* *
* @throws AuthorizationException * @throws AuthorizationException
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember * @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
* @throws \Throwable * @throws Throwable
* *
* @operationId mergeMember * @operationId mergeMember
*/ */

View File

@@ -55,6 +55,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use LogicException;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use Spatie\TemporaryDirectory\TemporaryDirectory; use Spatie\TemporaryDirectory\TemporaryDirectory;
@@ -190,8 +191,9 @@ class TimeEntryController extends Controller
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
if ($roundingType !== null && $roundingMinutes !== null) { if ($roundingType !== null && $roundingMinutes !== null) {
$select = array_diff($select, ['start', 'end']); $select = array_diff($select, ['start', 'end']);
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); // These SQL fragments are generated from a rounding enum and validated minute count.
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); // @phpstan-ignore argument.type
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); // @phpstan-ignore argument.type
} }
$timeEntriesQuery = TimeEntry::query() $timeEntriesQuery = TimeEntry::query()
->whereBelongsTo($organization, 'organization') ->whereBelongsTo($organization, 'organization')
@@ -265,7 +267,7 @@ class TimeEntryController extends Controller
} }
$viewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf.blade.php')); $viewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf.blade.php'));
if ($viewFile === false) { if ($viewFile === false) {
throw new \LogicException('View file not found'); throw new LogicException('View file not found');
} }
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries( $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
@@ -293,7 +295,7 @@ class TimeEntryController extends Controller
]); ]);
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf-footer.blade.php')); $footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf-footer.blade.php'));
if ($footerViewFile === false) { if ($footerViewFile === false) {
throw new \LogicException('View file not found'); throw new LogicException('View file not found');
} }
$footerHtml = Blade::render($footerViewFile); $footerHtml = Blade::render($footerViewFile);
if ($debug) { if ($debug) {
@@ -495,7 +497,7 @@ class TimeEntryController extends Controller
]); ]);
$viewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf.blade.php')); $viewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf.blade.php'));
if ($viewFile === false) { if ($viewFile === false) {
throw new \LogicException('View file not found'); throw new LogicException('View file not found');
} }
$html = Blade::render($viewFile, [ $html = Blade::render($viewFile, [
'aggregatedData' => $aggregatedData, 'aggregatedData' => $aggregatedData,
@@ -512,7 +514,7 @@ class TimeEntryController extends Controller
]); ]);
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf-footer.blade.php')); $footerViewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf-footer.blade.php'));
if ($footerViewFile === false) { if ($footerViewFile === false) {
throw new \LogicException('View file not found'); throw new LogicException('View file not found');
} }
$footerHtml = Blade::render($footerViewFile); $footerHtml = Blade::render($footerViewFile);
if ($debug) { if ($debug) {

View File

@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Web;
use App\Models\Organization; use App\Models\Organization;
use Brick\Money\Currency; use Brick\Money\Currency;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\IsoCurrencyProvider;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -53,7 +53,7 @@ class OrganizationController extends Controller
], ],
'currencies' => array_map(function (Currency $currency): string { 'currencies' => array_map(function (Currency $currency): string {
return $currency->getName(); return $currency->getName();
}, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()), }, IsoCurrencyProvider::getInstance()->getAvailableCurrencies()),
'permissions' => [ 'permissions' => [
'canDeleteTeam' => $this->hasPermission($organization, 'organizations:delete'), 'canDeleteTeam' => $this->hasPermission($organization, 'organizations:delete'),
'canUpdateTeam' => $this->hasPermission($organization, 'organizations:update'), 'canUpdateTeam' => $this->hasPermission($organization, 'organizations:update'),

View File

@@ -16,6 +16,7 @@ use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication; use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
use stdClass;
class UserProfileController extends Controller class UserProfileController extends Controller
{ {
@@ -103,7 +104,7 @@ class UserProfileController extends Controller
/** /**
* Get the current sessions. * Get the current sessions.
* *
* @return array<int, object{agent: array{is_desktop: bool, platform: string|null, browser: string|null}, ip_address: string, is_current_device: bool, last_active: string}&\stdClass> * @return array<int, object{agent: array{is_desktop: bool, platform: string|null, browser: string|null}, ip_address: string, is_current_device: bool, last_active: string}&stdClass>
*/ */
public function sessions(Request $request): array public function sessions(Request $request): array
{ {

View File

@@ -11,6 +11,7 @@ use App\Http\Middleware\EnsureEmailIsVerified;
use App\Http\Middleware\ForceHttps; use App\Http\Middleware\ForceHttps;
use App\Http\Middleware\ForceJsonResponse; use App\Http\Middleware\ForceJsonResponse;
use App\Http\Middleware\HandleInertiaRequests; use App\Http\Middleware\HandleInertiaRequests;
use App\Http\Middleware\PreventRequestForgery;
use App\Http\Middleware\PreventRequestsDuringMaintenance; use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated; use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\ShareInertiaData; use App\Http\Middleware\ShareInertiaData;
@@ -18,7 +19,6 @@ use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustHosts; use App\Http\Middleware\TrustHosts;
use App\Http\Middleware\TrustProxies; use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature; use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\RequirePassword; use Illuminate\Auth\Middleware\RequirePassword;
@@ -68,7 +68,7 @@ class Kernel extends HttpKernel
AddQueuedCookiesToResponse::class, AddQueuedCookiesToResponse::class,
StartSession::class, StartSession::class,
ShareErrorsFromSession::class, ShareErrorsFromSession::class,
VerifyCsrfToken::class, PreventRequestForgery::class,
SubstituteBindings::class, SubstituteBindings::class,
HandleInertiaRequests::class, HandleInertiaRequests::class,
ShareInertiaData::class, ShareInertiaData::class,

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Filament\Http\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class AuthenticateFilamentPanel extends Middleware
{
/**
* The admin panel has no login page of its own (it shares the app's
* session-based login), so Filament::getLoginUrl() always returns null.
* Fall back to the app's login route so guests are redirected instead
* of receiving an empty 401 response.
*/
protected function redirectTo($request): ?string
{
return parent::redirectTo($request) ?? ($request->expectsJson() ? null : route('login'));
}
}

View File

@@ -9,6 +9,7 @@ use App\Models\Organization;
use App\Service\BillingContract; use App\Service\BillingContract;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use LogicException;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class CheckOrganizationBlocked class CheckOrganizationBlocked
@@ -25,7 +26,7 @@ class CheckOrganizationBlocked
$organization = $request->route('organization'); $organization = $request->route('organization');
if (! ($organization instanceof Organization)) { if (! ($organization instanceof Organization)) {
throw new \LogicException('The organization must be loaded before this middleware.'); throw new LogicException('The organization must be loaded before this middleware.');
} }
/** @var BillingContract $billing */ /** @var BillingContract $billing */

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;
class PreventRequestForgery extends Middleware
{
/**
* The URIs that should be excluded.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Middleware\TrustHosts as BaseTrustHosts; use Illuminate\Http\Middleware\TrustHosts as BaseTrustHosts;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@@ -39,7 +40,7 @@ class TrustHosts extends BaseTrustHosts
} }
/** /**
* @param \Closure(Request): Response $next * @param Closure(Request):Response $next
*/ */
public function handle(Request $request, $next) public function handle(Request $request, $next)
{ {

View File

@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -13,11 +13,13 @@ use App\Enums\Weekday;
use App\Http\Requests\V1\BaseFormRequest; use App\Http\Requests\V1\BaseFormRequest;
use App\Models\Organization; use App\Models\Organization;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
use Closure;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule; use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use LogicException;
/** /**
* @property Organization $organization Organization from model binding * @property Organization $organization Organization from model binding
@@ -27,7 +29,7 @@ class ReportStoreRequest extends BaseFormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|ValidationRule|LegacyValidationRule|\Closure>> * @return array<string, array<string|ValidationRule|LegacyValidationRule|Closure>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -85,7 +87,7 @@ class ReportStoreRequest extends BaseFormRequest
], ],
'properties.client_ids.*' => [ 'properties.client_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -101,7 +103,7 @@ class ReportStoreRequest extends BaseFormRequest
], ],
'properties.project_ids.*' => [ 'properties.project_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -117,7 +119,7 @@ class ReportStoreRequest extends BaseFormRequest
], ],
'properties.tag_ids.*' => [ 'properties.tag_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -137,7 +139,7 @@ class ReportStoreRequest extends BaseFormRequest
], ],
'properties.task_ids.*' => [ 'properties.task_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -213,7 +215,7 @@ class ReportStoreRequest extends BaseFormRequest
{ {
$start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('properties.start')); $start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('properties.start'));
if ($start === null) { if ($start === null) {
throw new \LogicException('Start date validation is not working'); throw new LogicException('Start date validation is not working');
} }
return $start; return $start;
@@ -223,7 +225,7 @@ class ReportStoreRequest extends BaseFormRequest
{ {
$end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('properties.end')); $end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('properties.end'));
if ($end === null) { if ($end === null) {
throw new \LogicException('End date validation is not working'); throw new LogicException('End date validation is not working');
} }
return $end; return $end;

View File

@@ -19,11 +19,13 @@ use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Models\User; use App\Models\User;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
use LogicException;
/** /**
* @property Organization $organization * @property Organization $organization
@@ -33,7 +35,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|\Closure>> * @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|Closure>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -97,7 +99,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
], ],
'project_ids.*' => [ 'project_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -114,7 +116,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
], ],
'client_ids.*' => [ 'client_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -131,7 +133,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
], ],
'tag_ids.*' => [ 'tag_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -152,7 +154,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
], ],
'task_ids.*' => [ 'task_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -236,7 +238,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
{ {
$start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('start'), 'UTC'); $start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('start'), 'UTC');
if ($start === null) { if ($start === null) {
throw new \LogicException('Start date validation is not working'); throw new LogicException('Start date validation is not working');
} }
return $start; return $start;
@@ -246,7 +248,7 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
{ {
$end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC'); $end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC');
if ($end === null) { if ($end === null) {
throw new \LogicException('End date validation is not working'); throw new LogicException('End date validation is not working');
} }
return $end; return $end;

View File

@@ -17,6 +17,7 @@ use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Models\User; use App\Models\User;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@@ -31,7 +32,7 @@ class TimeEntryAggregateRequest extends BaseFormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|\Closure>> * @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|Closure>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -83,7 +84,7 @@ class TimeEntryAggregateRequest extends BaseFormRequest
], ],
'project_ids.*' => [ 'project_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -100,7 +101,7 @@ class TimeEntryAggregateRequest extends BaseFormRequest
], ],
'client_ids.*' => [ 'client_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -117,7 +118,7 @@ class TimeEntryAggregateRequest extends BaseFormRequest
], ],
'tag_ids.*' => [ 'tag_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -138,7 +139,7 @@ class TimeEntryAggregateRequest extends BaseFormRequest
], ],
'task_ids.*' => [ 'task_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }

View File

@@ -15,11 +15,13 @@ use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent; use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
use LogicException;
/** /**
* @property Organization $organization * @property Organization $organization
@@ -29,7 +31,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|\Closure>> * @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule|Closure>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -68,7 +70,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
], ],
'client_ids.*' => [ 'client_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -85,7 +87,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
], ],
'project_ids.*' => [ 'project_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -102,7 +104,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
], ],
'tag_ids.*' => [ 'tag_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -123,7 +125,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
], ],
'task_ids.*' => [ 'task_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -200,7 +202,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
{ {
$start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('start'), 'UTC'); $start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('start'), 'UTC');
if ($start === null) { if ($start === null) {
throw new \LogicException('Start date validation is not working'); throw new LogicException('Start date validation is not working');
} }
return $start; return $start;
@@ -210,7 +212,7 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
{ {
$end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC'); $end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC');
if ($end === null) { if ($end === null) {
throw new \LogicException('End date validation is not working'); throw new LogicException('End date validation is not working');
} }
return $end; return $end;

View File

@@ -15,6 +15,7 @@ use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Service\TimeEntryFilter; use App\Service\TimeEntryFilter;
use Closure;
use Illuminate\Contracts\Validation\Rule as RuleContract; use Illuminate\Contracts\Validation\Rule as RuleContract;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@@ -29,7 +30,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|ValidationRule|RuleContract|\Closure>> * @return array<string, array<string|ValidationRule|RuleContract|Closure>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -61,7 +62,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
], ],
'client_ids.*' => [ 'client_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -78,7 +79,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
], ],
'project_ids.*' => [ 'project_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -95,7 +96,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
], ],
'tag_ids.*' => [ 'tag_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }
@@ -116,7 +117,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
], ],
'task_ids.*' => [ 'task_ids.*' => [
'string', 'string',
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryFilter::NONE_VALUE) { if ($value === TimeEntryFilter::NONE_VALUE) {
return; return;
} }

View File

@@ -12,6 +12,7 @@ use App\Models\Project;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Service\PermissionStore; use App\Service\PermissionStore;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -26,7 +27,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|\Closure|ValidationRule|\Illuminate\Contracts\Validation\Rule>> * @return array<string, array<string|Closure|ValidationRule|\Illuminate\Contracts\Validation\Rule>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -94,7 +95,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
// Type of the time entry (work time or a break) // Type of the time entry (work time or a break)
'type' => [ 'type' => [
Rule::enum(TimeEntryType::class), Rule::enum(TimeEntryType::class),
function (string $attribute, mixed $value, \Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {
if ($value === TimeEntryType::Break->value && ! $this->organization->breaks_enabled) { if ($value === TimeEntryType::Break->value && ! $this->organization->breaks_enabled) {
$fail('Breaks are disabled for this organization.'); $fail('Breaks are disabled for this organization.');
} }

View File

@@ -13,6 +13,7 @@ use App\Models\Tag;
use App\Models\Task; use App\Models\Task;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Service\PermissionStore; use App\Service\PermissionStore;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -29,7 +30,7 @@ class TimeEntryUpdateRequest extends BaseFormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, array<string|\Closure|ValidationRule|\Illuminate\Contracts\Validation\Rule|ProhibitedIf|ConditionalRules>> * @return array<string, array<string|Closure|ValidationRule|\Illuminate\Contracts\Validation\Rule|ProhibitedIf|ConditionalRules>>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -105,7 +106,7 @@ class TimeEntryUpdateRequest extends BaseFormRequest
// Type of the time entry (work time or a break) // Type of the time entry (work time or a break)
'type' => [ 'type' => [
Rule::enum(TimeEntryType::class), Rule::enum(TimeEntryType::class),
function (string $attribute, mixed $value, \Closure $fail) use ($timeEntry): void { function (string $attribute, mixed $value, Closure $fail) use ($timeEntry): void {
// While breaks are disabled, entries that already are breaks may stay // While breaks are disabled, entries that already are breaks may stay
// breaks, but converting a work entry to a break is not allowed. // breaks, but converting a work entry to a break is not allowed.
if ($value === TimeEntryType::Break->value if ($value === TimeEntryType::Break->value

View File

@@ -19,7 +19,6 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract; use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
@@ -100,7 +99,7 @@ class Organization extends Model implements AuditableContract
/** /**
* Get all the users that belong to the team. * Get all the users that belong to the team.
* *
* @return BelongsToMany<User, $this, Pivot, 'membership'> * @return BelongsToMany<User, $this, Member, 'membership'>
*/ */
public function users(): BelongsToMany public function users(): BelongsToMany
{ {
@@ -133,7 +132,7 @@ class Organization extends Model implements AuditableContract
} }
/** /**
* @return BelongsToMany<User, $this, Pivot, 'membership'> * @return BelongsToMany<User, $this, Member, 'membership'>
*/ */
public function realUsers(): BelongsToMany public function realUsers(): BelongsToMany
{ {

View File

@@ -20,7 +20,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@@ -175,7 +174,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
} }
/** /**
* @return BelongsToMany<Organization, $this, Pivot, 'membership'> * @return BelongsToMany<Organization, $this, Member, 'membership'>
*/ */
public function organizations(): BelongsToMany public function organizations(): BelongsToMany
{ {
@@ -190,7 +189,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
} }
/** /**
* @return BelongsToMany<Organization, $this, Pivot, 'membership'> * @return BelongsToMany<Organization, $this, Member, 'membership'>
*/ */
public function ownedOrganizations(): BelongsToMany public function ownedOrganizations(): BelongsToMany
{ {

View File

@@ -25,7 +25,7 @@ use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme; use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow;
use Filament\Forms\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;

View File

@@ -9,17 +9,17 @@ use App\Filament\Widgets\ServerOverview;
use App\Filament\Widgets\TimeEntriesCreated; use App\Filament\Widgets\TimeEntriesCreated;
use App\Filament\Widgets\TimeEntriesImported; use App\Filament\Widgets\TimeEntriesImported;
use App\Filament\Widgets\UserRegistrations; use App\Filament\Widgets\UserRegistrations;
use Filament\Http\Middleware\Authenticate; use App\Http\Middleware\AuthenticateFilamentPanel;
use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Navigation\NavigationGroup; use Filament\Navigation\NavigationGroup;
use Filament\Pages; use Filament\Pages\Dashboard;
use Filament\Panel; use Filament\Panel;
use Filament\PanelProvider; use Filament\PanelProvider;
use Filament\Support\Colors\Color; use Filament\Support\Colors\Color;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
@@ -41,7 +41,7 @@ class AdminPanelProvider extends PanelProvider
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([ ->pages([
Pages\Dashboard::class, Dashboard::class,
]) ])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([
@@ -51,7 +51,6 @@ class AdminPanelProvider extends PanelProvider
TimeEntriesCreated::class, TimeEntriesCreated::class,
TimeEntriesImported::class, TimeEntriesImported::class,
]) ])
->viteTheme('resources/css/filament/admin/theme.css')
->plugins([ ->plugins([
EnvironmentIndicatorPlugin::make() EnvironmentIndicatorPlugin::make()
->color(fn () => match (App::environment()) { ->color(fn () => match (App::environment()) {
@@ -72,6 +71,9 @@ class AdminPanelProvider extends PanelProvider
NavigationGroup::make() NavigationGroup::make()
->label('Auth') ->label('Auth')
->collapsed(), ->collapsed(),
NavigationGroup::make()
->label('Self Hosting')
->collapsed(),
]) ])
->middleware([ ->middleware([
EncryptCookies::class, EncryptCookies::class,
@@ -79,13 +81,13 @@ class AdminPanelProvider extends PanelProvider
StartSession::class, StartSession::class,
AuthenticateSession::class, AuthenticateSession::class,
ShareErrorsFromSession::class, ShareErrorsFromSession::class,
VerifyCsrfToken::class, PreventRequestForgery::class,
SubstituteBindings::class, SubstituteBindings::class,
DisableBladeIconComponents::class, DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class, DispatchServingFilamentEvent::class,
]) ])
->authMiddleware([ ->authMiddleware([
Authenticate::class, AuthenticateFilamentPanel::class,
]); ]);
$modules = Module::allEnabled(); $modules = Module::allEnabled();

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Rules; namespace App\Rules;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\IsoCurrencyProvider;
use Closure; use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString; use Illuminate\Translation\PotentiallyTranslatedString;
@@ -24,7 +24,7 @@ class CurrencyRule implements ValidationRule
return; return;
} }
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies(); $currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
if (array_key_exists($value, $currencies)) { if (array_key_exists($value, $currencies)) {
return; return;
} }

View File

@@ -15,11 +15,18 @@ use App\Models\User;
use Exception; use Exception;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Log; use Log;
use Nwidart\Modules\Facades\Module;
use Throwable;
class ApiService class ApiService
{ {
private const string API_URL = 'https://app.solidtime.io/api/v1'; private const string API_URL = 'https://app.solidtime.io/api/v1';
private function isPremium(): bool
{
return Module::has('Invoicing');
}
public function checkForUpdate(): ?string public function checkForUpdate(): ?string
{ {
try { try {
@@ -30,6 +37,7 @@ class ApiService
'version' => config('app.version'), 'version' => config('app.version'),
'build' => config('app.build'), 'build' => config('app.build'),
'url' => config('app.url'), 'url' => config('app.url'),
'is_premium' => $this->isPremium(),
]); ]);
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) { if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
@@ -42,7 +50,7 @@ class ApiService
return null; return null;
} }
} catch (\Throwable $e) { } catch (Throwable $e) {
Log::warning('Failed to check for update', [ Log::warning('Failed to check for update', [
'message' => $e->getMessage(), 'message' => $e->getMessage(),
]); ]);
@@ -61,6 +69,7 @@ class ApiService
'version' => config('app.version'), 'version' => config('app.version'),
'build' => config('app.build'), 'build' => config('app.build'),
'url' => config('app.url'), 'url' => config('app.url'),
'is_premium' => $this->isPremium(),
// telemetry data // telemetry data
'user_count' => User::count(), 'user_count' => User::count(),
'organization_count' => Organization::count(), 'organization_count' => Organization::count(),

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service; namespace App\Service;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\IsoCurrencyProvider;
use Brick\Money\Money; use Brick\Money\Money;
class CurrencyService class CurrencyService
@@ -378,7 +378,7 @@ class CurrencyService
public function getRandomCurrencyCode(): string public function getRandomCurrencyCode(): string
{ {
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies(); $currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
$currencyCodes = array_keys($currencies); $currencyCodes = array_keys($currencies);
return $currencyCodes[array_rand($currencyCodes)]; return $currencyCodes[array_rand($currencyCodes)];

View File

@@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use RuntimeException;
class DashboardService class DashboardService
{ {
@@ -101,7 +102,7 @@ class DashboardService
$value1 = Carbon::createFromFormat('Y-m-d', $possibleDates->first(), $timeZone); $value1 = Carbon::createFromFormat('Y-m-d', $possibleDates->first(), $timeZone);
$value2 = Carbon::createFromFormat('Y-m-d', $possibleDates->last(), $timeZone); $value2 = Carbon::createFromFormat('Y-m-d', $possibleDates->last(), $timeZone);
if ($value2 === null || $value1 === null) { if ($value2 === null || $value1 === null) {
throw new \RuntimeException('Provided date is not valid'); throw new RuntimeException('Provided date is not valid');
} }
if ($value1->gt($value2)) { if ($value1->gt($value2)) {
$last = $value1; $last = $value1;
@@ -151,12 +152,13 @@ class DashboardService
$possibleDays = $this->lastDays($days, $timezone); $possibleDays = $this->lastDays($days, $timezone);
// The SQL fragment contains only a timezone offset calculated as an integer.
$query = TimeEntry::query() $query = TimeEntry::query()
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) ->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
->where('user_id', '=', $user->getKey()) ->where('user_id', '=', $user->getKey())
->where('organization_id', '=', $organization->getKey()) ->where('organization_id', '=', $organization->getKey())
->workTime() ->workTime()
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) ->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
->orderBy('date'); ->orderBy('date');
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
@@ -193,12 +195,13 @@ class DashboardService
} }
$possibleDays = $this->daysOfThisWeek($timezone, $user->week_start); $possibleDays = $this->daysOfThisWeek($timezone, $user->week_start);
// The SQL fragment contains only a timezone offset calculated as an integer.
$query = TimeEntry::query() $query = TimeEntry::query()
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) ->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
->where('user_id', '=', $user->getKey()) ->where('user_id', '=', $user->getKey())
->where('organization_id', '=', $organization->getKey()) ->where('organization_id', '=', $organization->getKey())
->workTime() ->workTime()
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) ->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
->orderBy('date'); ->orderBy('date');
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);

View File

@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use InvalidArgumentException;
class ReportPropertiesDto implements Castable class ReportPropertiesDto implements Castable
{ {
@@ -101,15 +102,15 @@ class ReportPropertiesDto implements Castable
public function get(Model $model, string $key, mixed $value, array $attributes): ReportPropertiesDto public function get(Model $model, string $key, mixed $value, array $attributes): ReportPropertiesDto
{ {
if (! is_string($value)) { if (! is_string($value)) {
throw new \InvalidArgumentException('The given value is not a string'); throw new InvalidArgumentException('The given value is not a string');
} }
$data = json_decode($value, false); $data = json_decode($value, false);
if ($data === null) { if ($data === null) {
throw new \InvalidArgumentException('The given value is not a JSON string'); throw new InvalidArgumentException('The given value is not a JSON string');
} }
foreach (self::REQUIRED_PROPERTIES as $property) { foreach (self::REQUIRED_PROPERTIES as $property) {
if (! property_exists($data, $property)) { if (! property_exists($data, $property)) {
throw new \InvalidArgumentException('The given JSON string does not contain the required property "'.$property.'"'); throw new InvalidArgumentException('The given JSON string does not contain the required property "'.$property.'"');
} }
} }
$dto = new ReportPropertiesDto; $dto = new ReportPropertiesDto;
@@ -145,7 +146,7 @@ class ReportPropertiesDto implements Castable
public function set(Model $model, string $key, mixed $value, array $attributes): string public function set(Model $model, string $key, mixed $value, array $attributes): string
{ {
if (! ($value instanceof ReportPropertiesDto)) { if (! ($value instanceof ReportPropertiesDto)) {
throw new \InvalidArgumentException('The given value is not an instance of ReportPropertiesDto'); throw new InvalidArgumentException('The given value is not an instance of ReportPropertiesDto');
} }
$data = (object) [ $data = (object) [
@@ -171,7 +172,7 @@ class ReportPropertiesDto implements Castable
$jsonString = json_encode($data); $jsonString = json_encode($data);
if ($jsonString === false) { if ($jsonString === false) {
throw new \InvalidArgumentException('Could not encode the given data to a JSON string'); throw new InvalidArgumentException('Could not encode the given data to a JSON string');
} }
return $jsonString; return $jsonString;
@@ -188,10 +189,10 @@ class ReportPropertiesDto implements Castable
$collection = new Collection; $collection = new Collection;
foreach ($ids as $id) { foreach ($ids as $id) {
if (! is_string($id)) { if (! is_string($id)) {
throw new \InvalidArgumentException('The given ID is not a string'); throw new InvalidArgumentException('The given ID is not a string');
} }
if ($id !== TimeEntryFilter::NONE_VALUE && ! Str::isUuid($id)) { if ($id !== TimeEntryFilter::NONE_VALUE && ! Str::isUuid($id)) {
throw new \InvalidArgumentException('The given ID is not a valid UUID'); throw new InvalidArgumentException('The given ID is not a valid UUID');
} }
$collection->push($id); $collection->push($id);
} }

View File

@@ -9,6 +9,7 @@ use Closure;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use RuntimeException;
/** /**
* @template TModel of Model * @template TModel of Model
@@ -133,7 +134,7 @@ class ImportDatabaseHelper
{ {
$jsonData = json_encode($data); $jsonData = json_encode($data);
if ($jsonData === false) { if ($jsonData === false) {
throw new \RuntimeException('Failed to encode data to JSON'); throw new RuntimeException('Failed to encode data to JSON');
} }
return md5($jsonData); return md5($jsonData);
@@ -164,7 +165,7 @@ class ImportDatabaseHelper
return $this->createEntity($identifierData, $createValues, $externalIdentifier); return $this->createEntity($identifierData, $createValues, $externalIdentifier);
} else { } else {
throw new \RuntimeException('Not implemented'); throw new RuntimeException('Not implemented');
} }
} }

View File

@@ -9,13 +9,14 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Exception as CsvException; use League\Csv\Exception as CsvException;
use League\Csv\Reader; use League\Csv\Reader;
use Override;
class ClockifyProjectsImporter extends DefaultImporter class ClockifyProjectsImporter extends DefaultImporter
{ {
/** /**
* @throws ImportException * @throws ImportException
*/ */
#[\Override] #[Override]
public function importData(string $data, string $timezone): void public function importData(string $data, string $timezone): void
{ {
try { try {
@@ -129,13 +130,13 @@ class ClockifyProjectsImporter extends DefaultImporter
return $billableRateKey; return $billableRateKey;
} }
#[\Override] #[Override]
public function getName(): string public function getName(): string
{ {
return __('importer.clockify_projects.name'); return __('importer.clockify_projects.name');
} }
#[\Override] #[Override]
public function getDescription(): string public function getDescription(): string
{ {
return __('importer.clockify_projects.description'); return __('importer.clockify_projects.description');

View File

@@ -15,6 +15,7 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Exception as CsvException; use League\Csv\Exception as CsvException;
use League\Csv\Reader; use League\Csv\Reader;
use Override;
class ClockifyTimeEntriesImporter extends DefaultImporter class ClockifyTimeEntriesImporter extends DefaultImporter
{ {
@@ -44,7 +45,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
/** /**
* @throws ImportException * @throws ImportException
*/ */
#[\Override] #[Override]
public function importData(string $data, string $timezone): void public function importData(string $data, string $timezone): void
{ {
try { try {
@@ -262,13 +263,13 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
return null; return null;
} }
#[\Override] #[Override]
public function getName(): string public function getName(): string
{ {
return __('importer.clockify_time_entries.name'); return __('importer.clockify_time_entries.name');
} }
#[\Override] #[Override]
public function getDescription(): string public function getDescription(): string
{ {
return __('importer.clockify_time_entries.description'); return __('importer.clockify_time_entries.description');

View File

@@ -18,6 +18,7 @@ use App\Service\ColorService;
use App\Service\Import\ImportDatabaseHelper; use App\Service\Import\ImportDatabaseHelper;
use App\Service\TimezoneService; use App\Service\TimezoneService;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Override;
abstract class DefaultImporter implements ImporterContract abstract class DefaultImporter implements ImporterContract
{ {
@@ -193,7 +194,7 @@ abstract class DefaultImporter implements ImporterContract
} }
} }
#[\Override] #[Override]
public function getReport(): ReportDto public function getReport(): ReportDto
{ {
return new ReportDto( return new ReportDto(

View File

@@ -14,6 +14,7 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Exception as CsvException; use League\Csv\Exception as CsvException;
use League\Csv\Reader; use League\Csv\Reader;
use Override;
class GenericTimeEntriesImporter extends DefaultImporter class GenericTimeEntriesImporter extends DefaultImporter
{ {
@@ -59,7 +60,7 @@ class GenericTimeEntriesImporter extends DefaultImporter
/** /**
* @throws ImportException * @throws ImportException
*/ */
#[\Override] #[Override]
public function importData(string $data, string $timezone): void public function importData(string $data, string $timezone): void
{ {
try { try {
@@ -195,13 +196,13 @@ class GenericTimeEntriesImporter extends DefaultImporter
} }
} }
#[\Override] #[Override]
public function getName(): string public function getName(): string
{ {
return __('importer.generic_time_entries.name'); return __('importer.generic_time_entries.name');
} }
#[\Override] #[Override]
public function getDescription(): string public function getDescription(): string
{ {
return __('importer.generic_time_entries.description'); return __('importer.generic_time_entries.description');

View File

@@ -7,6 +7,7 @@ namespace App\Service\Import\Importers;
use Exception; use Exception;
use League\Csv\Exception as CsvException; use League\Csv\Exception as CsvException;
use League\Csv\Reader; use League\Csv\Reader;
use Override;
class HarvestClientsImporter extends DefaultImporter class HarvestClientsImporter extends DefaultImporter
{ {
@@ -20,7 +21,7 @@ class HarvestClientsImporter extends DefaultImporter
/** /**
* @throws ImportException * @throws ImportException
*/ */
#[\Override] #[Override]
public function importData(string $data, string $timezone): void public function importData(string $data, string $timezone): void
{ {
try { try {
@@ -62,13 +63,13 @@ class HarvestClientsImporter extends DefaultImporter
} }
} }
#[\Override] #[Override]
public function getName(): string public function getName(): string
{ {
return __('importer.harvest_clients.name'); return __('importer.harvest_clients.name');
} }
#[\Override] #[Override]
public function getDescription(): string public function getDescription(): string
{ {
return __('importer.harvest_clients.description'); return __('importer.harvest_clients.description');

View File

@@ -8,6 +8,7 @@ use Exception;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Exception as CsvException; use League\Csv\Exception as CsvException;
use League\Csv\Reader; use League\Csv\Reader;
use Override;
class HarvestProjectsImporter extends DefaultImporter class HarvestProjectsImporter extends DefaultImporter
{ {
@@ -24,7 +25,7 @@ class HarvestProjectsImporter extends DefaultImporter
/** /**
* @throws ImportException * @throws ImportException
*/ */
#[\Override] #[Override]
public function importData(string $data, string $timezone): void public function importData(string $data, string $timezone): void
{ {
try { try {
@@ -93,13 +94,13 @@ class HarvestProjectsImporter extends DefaultImporter
} }
} }
#[\Override] #[Override]
public function getName(): string public function getName(): string
{ {
return __('importer.harvest_projects.name'); return __('importer.harvest_projects.name');
} }
#[\Override] #[Override]
public function getDescription(): string public function getDescription(): string
{ {
return __('importer.harvest_projects.description'); return __('importer.harvest_projects.description');

View File

@@ -4,4 +4,6 @@ declare(strict_types=1);
namespace App\Service\Import\Importers; namespace App\Service\Import\Importers;
class ImportException extends \Exception {} use Exception;
class ImportException extends Exception {}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Service\Import\Importers; namespace App\Service\Import\Importers;
use InvalidArgumentException;
class ImporterProvider class ImporterProvider
{ {
/** /**
@@ -49,7 +51,7 @@ class ImporterProvider
public function getImporter(string $type): ImporterContract public function getImporter(string $type): ImporterContract
{ {
if (! array_key_exists($type, $this->importers)) { if (! array_key_exists($type, $this->importers)) {
throw new \InvalidArgumentException('Invalid importer type'); throw new InvalidArgumentException('Invalid importer type');
} }
return new $this->importers[$type]; return new $this->importers[$type];

View File

@@ -14,6 +14,7 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Exception as CsvException; use League\Csv\Exception as CsvException;
use League\Csv\Reader; use League\Csv\Reader;
use Override;
class TogglTimeEntriesImporter extends DefaultImporter class TogglTimeEntriesImporter extends DefaultImporter
{ {
@@ -43,7 +44,7 @@ class TogglTimeEntriesImporter extends DefaultImporter
/** /**
* @throws ImportException * @throws ImportException
*/ */
#[\Override] #[Override]
public function importData(string $data, string $timezone): void public function importData(string $data, string $timezone): void
{ {
try { try {
@@ -193,13 +194,13 @@ class TogglTimeEntriesImporter extends DefaultImporter
} }
} }
#[\Override] #[Override]
public function getName(): string public function getName(): string
{ {
return __('importer.toggl_time_entries.name'); return __('importer.toggl_time_entries.name');
} }
#[\Override] #[Override]
public function getDescription(): string public function getDescription(): string
{ {
return __('importer.toggl_time_entries.description'); return __('importer.toggl_time_entries.description');

View File

@@ -14,6 +14,7 @@ use League\Csv\CannotInsertRecord;
use League\Csv\Exception; use League\Csv\Exception;
use League\Csv\UnavailableStream; use League\Csv\UnavailableStream;
use League\Csv\Writer; use League\Csv\Writer;
use LogicException;
use Spatie\TemporaryDirectory\TemporaryDirectory; use Spatie\TemporaryDirectory\TemporaryDirectory;
/** /**
@@ -116,7 +117,7 @@ abstract class CsvExport
private function validateRow(array $row): void private function validateRow(array $row): void
{ {
if (array_keys($row) !== static::HEADER) { if (array_keys($row) !== static::HEADER) {
throw new \LogicException('Invalid row'); throw new LogicException('Invalid row');
} }
} }
} }

View File

@@ -20,6 +20,7 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
class TimeEntryAggregationService class TimeEntryAggregationService
{ {
@@ -74,8 +75,9 @@ class TimeEntryAggregationService
$startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes); $startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes);
$endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes); $endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes);
// SQL fragments come from enum cases, integer offsets, and formatted dates.
$timeEntriesQuery->selectRaw( $timeEntriesQuery->selectRaw(
($group1Select !== null ? $group1Select.' as group_1,' : ''). ($group1Select !== null ? $group1Select.' as group_1,' : ''). // @phpstan-ignore argument.type
($group2Select !== null ? $group2Select.' as group_2,' : ''). ($group2Select !== null ? $group2Select.' as group_2,' : '').
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. ' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost' ' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
@@ -104,7 +106,7 @@ class TimeEntryAggregationService
$baseTotalsPerGroup1Query = $baseTotalsQuery->clone(); $baseTotalsPerGroup1Query = $baseTotalsQuery->clone();
$baseTotalsPerGroup1 = $baseTotalsPerGroup1Query $baseTotalsPerGroup1 = $baseTotalsPerGroup1Query
->selectRaw( ->selectRaw(
$group1Select.' as group_1,'. $group1Select.' as group_1,'. // @phpstan-ignore argument.type
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. ' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost' ' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
) )
@@ -169,7 +171,7 @@ class TimeEntryAggregationService
// Reset selects and ordering on the cloned base query // Reset selects and ordering on the cloned base query
$baseTotals = $baseTotalsQuery $baseTotals = $baseTotalsQuery
->selectRaw( ->selectRaw(
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. ' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. // @phpstan-ignore argument.type
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost' ' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
) )
->first(); ->first();
@@ -526,7 +528,7 @@ class TimeEntryAggregationService
public function timeSlotsBetween(Carbon $start, Carbon $end, string $timezone, Weekday $startOfWeek, TimeEntryAggregationTypeInterval $interval, string $format): Collection public function timeSlotsBetween(Carbon $start, Carbon $end, string $timezone, Weekday $startOfWeek, TimeEntryAggregationTypeInterval $interval, string $format): Collection
{ {
if ($start->gt($end)) { if ($start->gt($end)) {
throw new \InvalidArgumentException('Start date must be before end date'); throw new InvalidArgumentException('Start date must be before end date');
} }
$slots = new Collection; $slots = new Collection;
$current = $start->copy()->timezone($timezone); $current = $start->copy()->timezone($timezone);
@@ -539,7 +541,7 @@ class TimeEntryAggregationService
} elseif ($interval === TimeEntryAggregationTypeInterval::Year) { } elseif ($interval === TimeEntryAggregationTypeInterval::Year) {
$current->startOfYear(); $current->startOfYear();
} else { } else {
throw new \InvalidArgumentException('Invalid interval'); throw new InvalidArgumentException('Invalid interval');
} }
while ($current->lt($end)) { while ($current->lt($end)) {

View File

@@ -6,6 +6,7 @@ namespace App\Service;
use App\Models\User; use App\Models\User;
use Carbon\CarbonTimeZone; use Carbon\CarbonTimeZone;
use Exception;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -287,7 +288,7 @@ class TimezoneService
{ {
try { try {
return new CarbonTimeZone($user->timezone); return new CarbonTimeZone($user->timezone);
} catch (\Exception $e) { } catch (Exception $e) {
Log::error('User has a invalid timezone', [ Log::error('User has a invalid timezone', [
'user_id' => $user->getKey(), 'user_id' => $user->getKey(),
'timezone' => $user->timezone, 'timezone' => $user->timezone,

Some files were not shown because too many files have changed in this diff Show More