Compare commits

..

2 Commits

Author SHA1 Message Date
Gregor Vostrak
eb7233dfba anchor cross-day calendar e2e tests to Wednesday and Thursday
The tests used today and tomorrow (or yesterday) and skipped when those fell outside the visible week. The Saturday skips were also wrong for the Monday week start e2e users get, so the tests failed whenever CI ran on a Sunday. Using Wednesday and Thursday of the current week keeps both days visible whichever day the suite runs, so the day-of-week skips are gone.
2026-09-28 17:18:40 +02:00
Gregor Vostrak
398a81798b move project task count into its own sortable column
Also removes the ring around the project color dot, shrinks it slightly and tightens its spacing to the name.
2026-09-28 17:18:40 +02:00
193 changed files with 2855 additions and 6353 deletions

View File

@@ -6,7 +6,6 @@ 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
@@ -65,6 +64,3 @@ GOTENBERG_URL=http://localhost:3000
# Octane # Octane
OCTANE_SERVER=frankenphp OCTANE_SERVER=frankenphp
# Inertia
INERTIA_SSR_ENABLED=false

View File

@@ -88,6 +88,3 @@ 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,5 +13,3 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=pgsql DB_CONNECTION=pgsql
INERTIA_SSR_ENABLED=false

View File

@@ -141,7 +141,7 @@ jobs:
${{ env.DOCKER_REPO }} ${{ env.DOCKER_REPO }}
- name: "Login to solidtime OnPremise Registry" - name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
registry: registry.on-premise.solidtime.io registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }} username: ${{ secrets.ONPREMISE_USERNAME }}
@@ -195,7 +195,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: "Login to solidtime OnPremise Registry" - name: "Login to solidtime OnPremise Registry"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
registry: registry.on-premise.solidtime.io registry: registry.on-premise.solidtime.io
username: ${{ secrets.ONPREMISE_USERNAME }} username: ${{ secrets.ONPREMISE_USERNAME }}

View File

@@ -97,7 +97,7 @@ jobs:
- name: "Install dependencies in billing extension" - name: "Install dependencies in billing extension"
uses: php-actions/composer@v6 uses: php-actions/composer@v6
env: env:
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "${{ secrets.LARAVEL_SPARK_USERNAME }}", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}' COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "gregor@vostrak.at", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
with: with:
working_dir: "extensions/Billing" working_dir: "extensions/Billing"
command: install command: install
@@ -177,7 +177,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: "Login to GitHub Container Registry" - name: "Login to GitHub Container Registry"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
registry: rg.fr-par.scw.cloud/solidtime registry: rg.fr-par.scw.cloud/solidtime
username: nologin username: nologin

View File

@@ -117,13 +117,13 @@ jobs:
${{ env.GHCR_REPO }} ${{ env.GHCR_REPO }}
- name: "Login to Docker Hub Container Registry" - name: "Login to Docker Hub Container Registry"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GitHub Container Registry" - name: "Login to GitHub Container Registry"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -177,13 +177,13 @@ jobs:
merge-multiple: true merge-multiple: true
- name: "Login to Docker Hub" - name: "Login to Docker Hub"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Login to GHCR" - name: "Login to GHCR"
uses: docker/login-action@v4.6.0 uses: docker/login-action@v4.5.2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

View File

@@ -1,31 +0,0 @@
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,136 +0,0 @@
name: PHPUnit Tests - Extensions
on: push
permissions:
contents: read
jobs:
phpunit-extensions:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
postgres_version: [ 15, 16, 17 ]
services:
pgsql_test:
image: postgres:${{ matrix.postgres_version }}
env:
PGPASSWORD: 'root'
POSTGRES_DB: 'laravel'
POSTGRES_USER: 'root'
POSTGRES_PASSWORD: 'root'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
gotenberg:
image: gotenberg/gotenberg:8
ports:
- 3000:3000
options: >-
--health-cmd "curl --silent --fail http://localhost:3000/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: "Checkout code"
uses: actions/checkout@v7
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: pcov
- uses: actions/setup-node@v7
with:
node-version: '20.x'
- name: "Read extension manifest"
id: extension-manifest
run: |
{
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
echo "services_repository=$(jq -r '.Services.repository' 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_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
} >> "$GITHUB_OUTPUT"
- name: "Checkout billing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
path: extensions/Billing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
- name: "Install composer dependencies in billing extension"
working-directory: extensions/Billing
env:
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "contact@solidtime.io", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
run: composer install -n --prefer-dist --ignore-platform-reqs
- name: "Install npm dependencies in billing extension"
run: cd extensions/Billing && npm ci
- name: "Checkout services extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.services_repository }}
ref: ${{ steps.extension-manifest.outputs.services_ref }}
path: extensions/Services
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
- name: "Install composer dependencies in services extension"
working-directory: extensions/Services
run: composer install -n --prefer-dist --ignore-platform-reqs
- name: "Install npm dependencies in services extension"
run: cd extensions/Services && npm ci
- name: "Checkout invoicing extension"
uses: actions/checkout@v7
with:
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
path: extensions/Invoicing
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
- name: "Install composer dependencies in invoicing extension"
working-directory: extensions/Invoicing
run: composer install -n --prefer-dist --ignore-platform-reqs
- name: "Install npm dependencies in invoicing extension"
run: cd extensions/Invoicing && npm ci
- name: "Run composer install"
run: composer install -n --prefer-dist --ignore-platform-reqs
- name: "Activate billing extension"
run: php artisan module:enable Billing
- name: "Activate services extension"
run: php artisan module:enable Services
- name: "Activate invoicing extension"
run: php artisan module:enable Invoicing
- name: "Install dependencies"
run: npm ci
- name: "Build Frontend"
run: npm run build
- name: "Prepare Laravel Application"
run: |
cp .env.ci .env
php artisan key:generate
php artisan passport:keys
- name: "Run PHPUnit"
run: php artisan test extensions/Billing/tests extensions/Services/tests extensions/Invoicing/tests --stop-on-failure

View File

@@ -68,7 +68,7 @@ jobs:
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
- name: "Upload coverage reports to Codecov" - name: "Upload coverage reports to Codecov"
uses: codecov/codecov-action@v7.1.1 uses: codecov/codecov-action@v7.0.0
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
slug: solidtime-io/solidtime slug: solidtime-io/solidtime

2
.gitignore vendored
View File

@@ -6,7 +6,6 @@ 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
@@ -44,5 +43,4 @@ yarn-error.log
/data /data
/config/caddy /config/caddy
/config/composer /config/composer
/config/psysh
/AGENTS.md /AGENTS.md

View File

@@ -7,7 +7,6 @@ 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.
@@ -27,7 +26,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,7 +10,6 @@ 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
{ {
@@ -102,7 +101,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 phpseclib4\Crypt\RSA; use phpseclib3\Crypt\RSA;
class SelfHostGenerateKeysCommand extends Command class SelfHostGenerateKeysCommand extends Command
{ {

View File

@@ -4,8 +4,6 @@ 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;
@@ -34,7 +32,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

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

View File

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

View File

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

View File

@@ -9,10 +9,7 @@ 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\CreateOrganization; use App\Filament\Resources\OrganizationResource\Pages;
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;
@@ -23,45 +20,38 @@ 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 Exception; use Filament\Forms;
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\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\Schemas\Schema; use Filament\Tables;
use Filament\Tables\Columns\IconColumn; use Filament\Tables\Actions\Action;
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|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2'; protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
protected static string|\UnitEnum|null $navigationGroup = 'Users'; protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 7; protected static ?int $navigationSort = 7;
public static function form(Schema $schema): Schema public static function form(Form $form): Form
{ {
return $schema return $form
->columns(1) ->columns(1)
->components([ ->schema([
TextInput::make('name') Forms\Components\TextInput::make('name')
->label('Name') ->label('Name')
->required() ->required()
->maxLength(255), ->maxLength(255),
Toggle::make('personal_team') Forms\Components\Toggle::make('personal_team')
->label('Is personal?') ->label('Is personal?')
->hiddenOn(['create']) ->hiddenOn(['create'])
->required(), ->required(),
@@ -89,7 +79,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().')';
@@ -99,7 +89,7 @@ class OrganizationResource extends Resource
}) })
->required() ->required()
->searchable(), ->searchable(),
TextInput::make('billable_rate') Forms\Components\TextInput::make('billable_rate')
->label('Billable rate (in Cents)') ->label('Billable rate (in Cents)')
->nullable() ->nullable()
->rules([ ->rules([
@@ -109,11 +99,11 @@ class OrganizationResource extends Resource
'max:2147483647', 'max:2147483647',
]) ])
->numeric(), ->numeric(),
DateTimePicker::make('created_at') Forms\Components\DateTimePicker::make('created_at')
->label('Created At') ->label('Created At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
DateTimePicker::make('updated_at') Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At') ->label('Updated At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
@@ -127,7 +117,7 @@ class OrganizationResource extends Resource
TextColumn::make('name') TextColumn::make('name')
->searchable() ->searchable()
->sortable(), ->sortable(),
IconColumn::make('personal_team') Tables\Columns\IconColumn::make('personal_team')
->boolean() ->boolean()
->label('Is personal?') ->label('Is personal?')
->sortable(), ->sortable(),
@@ -148,9 +138,9 @@ class OrganizationResource extends Resource
->filters([ ->filters([
// //
]) ])
->recordActions([ ->actions([
EditAction::make(), Tables\Actions\EditAction::make(),
DeleteAction::make() Tables\Actions\DeleteAction::make()
->using(function (Organization $record): void { ->using(function (Organization $record): void {
app(DeletionService::class)->deleteOrganization($record); app(DeletionService::class)->deleteOrganization($record);
}), }),
@@ -168,7 +158,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')
@@ -182,13 +172,9 @@ 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 {
try { $file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
} catch (UnableToReadFile) {
$file = null;
}
if ($file === null) { if ($file === null) {
throw new ImportException('File not found'); throw new \Exception('File not found');
} }
/** @var string $timezone */ /** @var string $timezone */
$timezone = $data['timezone']; $timezone = $data['timezone'];
@@ -223,8 +209,8 @@ class OrganizationResource extends Resource
} }
}) })
->tooltip(fn (Organization $record): string => 'Import into '.$record->name) ->tooltip(fn (Organization $record): string => 'Import into '.$record->name)
->schema([ ->form([
FileUpload::make('file') Forms\Components\FileUpload::make('file')
->label('File') ->label('File')
->required(), ->required(),
Select::make('type') Select::make('type')
@@ -244,7 +230,7 @@ class OrganizationResource extends Resource
->required(), ->required(),
]), ]),
]) ])
->toolbarActions([ ->bulkActions([
]); ]);
} }
@@ -259,10 +245,10 @@ class OrganizationResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => ListOrganizations::route('/'), 'index' => Pages\ListOrganizations::route('/'),
'create' => CreateOrganization::route('/create'), 'create' => Pages\CreateOrganization::route('/create'),
'edit' => EditOrganization::route('/{record}/edit'), 'edit' => Pages\EditOrganization::route('/{record}/edit'),
'view' => ViewOrganization::route('/{record}'), 'view' => Pages\ViewOrganization::route('/{record}'),
]; ];
} }
} }

View File

@@ -5,7 +5,6 @@ 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
@@ -15,7 +14,7 @@ class EditOrganization extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
DeleteOrganization::make(), OrganizationResource\Actions\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\CreateAction; use Filament\Actions;
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 [
CreateAction::make() Actions\CreateAction::make()
->icon('heroicon-s-plus'), ->icon('heroicon-s-plus'),
]; ];
} }

View File

@@ -9,17 +9,12 @@ 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\Schemas\Schema; use Filament\Tables;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Actions\Action;
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;
@@ -30,10 +25,10 @@ class InvitationsRelationManager extends RelationManager
protected static ?string $title = 'Invitations'; protected static ?string $title = 'Invitations';
public function form(Schema $schema): Schema public function form(Form $form): Form
{ {
return $schema return $form
->components([ ->schema([
TextInput::make('email') TextInput::make('email')
->label('Email') ->label('Email')
->disabledOn(['edit']) ->disabledOn(['edit'])
@@ -58,33 +53,33 @@ class InvitationsRelationManager extends RelationManager
->modelLabel('Invitation') ->modelLabel('Invitation')
->pluralModelLabel('Invitations') ->pluralModelLabel('Invitations')
->columns([ ->columns([
TextColumn::make('email'), Tables\Columns\TextColumn::make('email'),
TextColumn::make('role'), Tables\Columns\TextColumn::make('role'),
]) ])
->headerActions([ ->headerActions([
CreateAction::make() Tables\Actions\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'], ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), auth()->user()); ->inviteUser($ownerRecord, $data['email'], Role::from($data['role']), auth()->user());
}), }),
]) ])
->recordActions([ ->actions([
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(),
])), ])),
EditAction::make(), Tables\Actions\EditAction::make(),
DeleteAction::make(), Tables\Actions\DeleteAction::make(),
]) ])
->toolbarActions([ ->bulkActions([
BulkActionGroup::make([ Tables\Actions\BulkActionGroup::make([
DetachBulkAction::make(), Tables\Actions\DetachBulkAction::make(),
]), ]),
]); ]);
} }

View File

@@ -12,15 +12,14 @@ 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\Schemas\Schema; use Filament\Tables;
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;
@@ -29,10 +28,10 @@ class UsersRelationManager extends RelationManager
{ {
protected static string $relationship = 'users'; protected static string $relationship = 'users';
public function form(Schema $schema): Schema public function form(Form $form): Form
{ {
return $schema return $form
->components([ ->schema([
Select::make('role') Select::make('role')
->options(Role::class), ->options(Role::class),
TextInput::make('billable_rate') TextInput::make('billable_rate')
@@ -76,17 +75,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, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true); app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true);
}), }),
]) ])
->recordActions([ ->actions([
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(),
])), ])),
EditAction::make() Tables\Actions\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();
@@ -98,10 +97,9 @@ class UsersRelationManager extends RelationManager
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member); app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
} }
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']); if ($data['role'] !== $member->role) {
if ($newRole->value !== $member->role) {
try { try {
app(MemberService::class)->changeRole($member, $organization, $newRole, true); app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
} catch (ApiException $exception) { } catch (ApiException $exception) {
Notification::make() Notification::make()
->danger() ->danger()
@@ -115,7 +113,7 @@ class UsersRelationManager extends RelationManager
return $record; return $record;
}), }),
DetachAction::make() Tables\Actions\DetachAction::make()
->using(function (User $record): void { ->using(function (User $record): void {
/** @var Organization $organization */ /** @var Organization $organization */
$organization = $this->getOwnerRecord(); $organization = $this->getOwnerRecord();
@@ -135,7 +133,7 @@ class UsersRelationManager extends RelationManager
} }
}), }),
]) ])
->toolbarActions([ ->bulkActions([
]); ]);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,30 +6,21 @@ 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\CreateUser; use App\Filament\Resources\UserResource\Pages;
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\Actions\BulkAction; use Filament\Forms;
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\Components\Toggle; use Filament\Forms\Form;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Tables;
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;
@@ -37,26 +28,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\Actions\Impersonate; use STS\FilamentImpersonate\Tables\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|\BackedEnum|null $navigationIcon = 'heroicon-o-user'; protected static ?string $navigationIcon = 'heroicon-o-user';
protected static string|\UnitEnum|null $navigationGroup = 'Users'; protected static ?string $navigationGroup = 'Users';
protected static ?int $navigationSort = 6; protected static ?int $navigationSort = 6;
public static function form(Schema $schema): Schema public static function form(Form $form): Form
{ {
/** @var User|null $record */ /** @var User|null $record */
$record = $schema->getRecord(); $record = $form->getRecord();
return $schema return $form
->columns(1) ->columns(1)
->components([ ->schema([
TextInput::make('id') TextInput::make('id')
->label('ID') ->label('ID')
->disabled() ->disabled()
@@ -78,23 +69,23 @@ class UserResource extends Resource
'email:rfc,strict', 'email:rfc,strict',
]) ])
->maxLength(255), ->maxLength(255),
Toggle::make('is_placeholder') Forms\Components\Toggle::make('is_placeholder')
->label('Is Placeholder?') ->label('Is Placeholder?')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabledOn(['edit']), ->disabledOn(['edit']),
DateTimePicker::make('email_verified_at') Forms\Components\DateTimePicker::make('email_verified_at')
->label('Email Verified At') ->label('Email Verified At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->nullable(), ->nullable(),
Toggle::make('is_email_verified') Forms\Components\Toggle::make('is_email_verified')
->label('Email Verified?') ->label('Email Verified?')
->visibleOn(['create']), ->visibleOn(['create']),
Select::make('timezone') Forms\Components\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(),
Select::make('week_start') Forms\Components\Select::make('week_start')
->label('Week Start') ->label('Week Start')
->options(Weekday::class) ->options(Weekday::class)
->required(), ->required(),
@@ -112,10 +103,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),
Select::make('currency') Forms\Components\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().')';
@@ -126,11 +117,11 @@ class UserResource extends Resource
->required() ->required()
->visibleOn(['create']) ->visibleOn(['create'])
->searchable(), ->searchable(),
DateTimePicker::make('created_at') Forms\Components\DateTimePicker::make('created_at')
->label('Created At') ->label('Created At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
DateTimePicker::make('updated_at') Forms\Components\DateTimePicker::make('updated_at')
->label('Updated At') ->label('Updated At')
->hiddenOn(['create']) ->hiddenOn(['create'])
->disabled(), ->disabled(),
@@ -141,25 +132,25 @@ class UserResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('name') Tables\Columns\TextColumn::make('name')
->searchable() ->searchable()
->sortable(), ->sortable(),
TextColumn::make('email') Tables\Columns\TextColumn::make('email')
->icon('heroicon-m-envelope') ->icon('heroicon-m-envelope')
->searchable() ->searchable()
->sortable(), ->sortable(),
IconColumn::make('is_real_user') Tables\Columns\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(),
IconColumn::make('email_verified') Tables\Columns\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(),
TextColumn::make('created_at') Tables\Columns\TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable(), ->sortable(),
TextColumn::make('updated_at') Tables\Columns\TextColumn::make('updated_at')
->dateTime() ->dateTime()
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
@@ -187,7 +178,7 @@ class UserResource extends Resource
->attribute('email_verified_at') ->attribute('email_verified_at')
->nullable(), ->nullable(),
]) ])
->recordActions([ ->actions([
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();
@@ -200,8 +191,8 @@ class UserResource extends Resource
app(UserService::class)->switchCurrentOrganization($record, $organization); app(UserService::class)->switchCurrentOrganization($record, $organization);
} }
}), }),
EditAction::make(), Tables\Actions\EditAction::make(),
DeleteAction::make() Tables\Actions\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 {
@@ -216,8 +207,8 @@ class UserResource extends Resource
} }
}), }),
]) ])
->toolbarActions([ ->bulkActions([
BulkAction::make('Resend verification email') Tables\Actions\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) {
@@ -239,10 +230,10 @@ class UserResource extends Resource
public static function getPages(): array public static function getPages(): array
{ {
return [ return [
'index' => ListUsers::route('/'), 'index' => Pages\ListUsers::route('/'),
'create' => CreateUser::route('/create'), 'create' => Pages\CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'), 'edit' => Pages\EditUser::route('/{record}/edit'),
'view' => ViewUser::route('/{record}'), 'view' => Pages\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'],
$data['week_start'] instanceof Weekday ? $data['week_start'] : Weekday::from($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,9 +5,8 @@ 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\Actions\Impersonate; use STS\FilamentImpersonate\Pages\Actions\Impersonate;
class EditUser extends EditRecord class EditUser extends EditRecord
{ {
@@ -17,7 +16,7 @@ class EditUser extends EditRecord
{ {
return [ return [
Impersonate::make()->record($this->getRecord()), Impersonate::make()->record($this->getRecord()),
DeleteUser::make(), UserResource\Actions\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\CreateAction; use Filament\Actions;
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 [
CreateAction::make() Actions\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\Actions\Impersonate; use STS\FilamentImpersonate\Pages\Actions\Impersonate;
class ViewUser extends ViewRecord class ViewUser extends ViewRecord
{ {

View File

@@ -11,13 +11,12 @@ 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\Schemas\Schema; use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
@@ -25,10 +24,10 @@ class OrganizationsRelationManager extends RelationManager
{ {
protected static string $relationship = 'organizations'; protected static string $relationship = 'organizations';
public function form(Schema $schema): Schema public function form(Form $form): Form
{ {
return $schema return $form
->components([ ->schema([
Select::make('role') Select::make('role')
->options(Role::class), ->options(Role::class),
]); ]);
@@ -47,22 +46,21 @@ class OrganizationsRelationManager extends RelationManager
]) ])
->headerActions([ ->headerActions([
]) ])
->recordActions([ ->actions([
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(),
])), ])),
EditAction::make() Tables\Actions\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');
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']); if ($data['role'] !== $member->role) {
if ($newRole->value !== $member->role) {
try { try {
app(MemberService::class)->changeRole($member, $record, $newRole, true); app(MemberService::class)->changeRole($member, $record, Role::from($data['role']), true);
} catch (ApiException $exception) { } catch (ApiException $exception) {
Notification::make() Notification::make()
->danger() ->danger()
@@ -76,7 +74,7 @@ class OrganizationsRelationManager extends RelationManager
return $record; return $record;
}), }),
DetachAction::make() Tables\Actions\DetachAction::make()
->using(function (Organization $record): void { ->using(function (Organization $record): void {
/** @var User $user */ /** @var User $user */
$user = $this->getOwnerRecord(); $user = $this->getOwnerRecord();
@@ -96,7 +94,7 @@ class OrganizationsRelationManager extends RelationManager
} }
}), }),
]) ])
->toolbarActions([ ->bulkActions([
]); ]);
} }
} }

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\Actions\Action; use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema; use Filament\Tables;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Actions\Action;
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(Schema $schema): Schema public function form(Form $form): Form
{ {
return $schema return $form
->components([ ->schema([
]); ]);
} }
@@ -30,14 +30,14 @@ class OwnedOrganizationsRelationManager extends RelationManager
return $table return $table
->recordTitleAttribute('name') ->recordTitleAttribute('name')
->columns([ ->columns([
TextColumn::make('name'), Tables\Columns\TextColumn::make('name'),
]) ])
->filters([ ->filters([
// //
]) ])
->headerActions([ ->headerActions([
]) ])
->recordActions([ ->actions([
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(),
]) ])
->toolbarActions([ ->bulkActions([
]); ]);
} }
} }

View File

@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Cache;
class ServerOverview extends Widget class ServerOverview extends Widget
{ {
protected string $view = 'filament.widgets.server-overview'; protected static 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 ?string $heading = 'Time Entries Created'; protected static ?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' => $this->heading, 'label' => self::$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 ?string $heading = 'Time Entries Imported'; protected static ?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' => $this->heading, 'label' => self::$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 ?string $heading = 'User Registrations'; protected static ?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' => $this->heading, 'label' => self::$heading,
'data' => $data->map(fn (TrendValue $value) => $value->aggregate), 'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
], ],
], ],

View File

@@ -14,7 +14,6 @@ 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
{ {
@@ -63,7 +62,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,7 +33,6 @@ 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
{ {
@@ -147,7 +146,7 @@ class MemberController extends Controller
* *
* @throws AuthorizationException * @throws AuthorizationException
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember * @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
* @throws Throwable * @throws \Throwable
* *
* @operationId mergeMember * @operationId mergeMember
*/ */

View File

@@ -55,7 +55,6 @@ 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;
@@ -191,9 +190,8 @@ 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']);
// These SQL fragments are generated from a rounding enum and validated minute count. $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
$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');
$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')
@@ -267,7 +265,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(
@@ -295,7 +293,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) {
@@ -497,7 +495,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,
@@ -514,7 +512,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,7 +16,6 @@ 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
{ {
@@ -104,7 +103,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,7 +11,6 @@ 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;
@@ -19,6 +18,7 @@ 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,
PreventRequestForgery::class, VerifyCsrfToken::class,
SubstituteBindings::class, SubstituteBindings::class,
HandleInertiaRequests::class, HandleInertiaRequests::class,
ShareInertiaData::class, ShareInertiaData::class,

View File

@@ -1,22 +0,0 @@
<?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,7 +9,6 @@ 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
@@ -26,7 +25,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

@@ -1,19 +0,0 @@
<?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,7 +4,6 @@ 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;
@@ -40,7 +39,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

@@ -0,0 +1,19 @@
<?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,13 +13,11 @@ 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
@@ -29,7 +27,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
{ {
@@ -87,7 +85,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;
} }
@@ -103,7 +101,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;
} }
@@ -119,7 +117,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;
} }
@@ -139,7 +137,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;
} }
@@ -215,7 +213,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;
@@ -225,7 +223,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,13 +19,11 @@ 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
@@ -35,7 +33,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
{ {
@@ -99,7 +97,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;
} }
@@ -116,7 +114,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;
} }
@@ -133,7 +131,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;
} }
@@ -154,7 +152,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;
} }
@@ -238,7 +236,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;
@@ -248,7 +246,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,7 +17,6 @@ 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;
@@ -32,7 +31,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
{ {
@@ -84,7 +83,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;
} }
@@ -101,7 +100,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;
} }
@@ -118,7 +117,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;
} }
@@ -139,7 +138,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,13 +15,11 @@ 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
@@ -31,7 +29,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
{ {
@@ -70,7 +68,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;
} }
@@ -87,7 +85,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;
} }
@@ -104,7 +102,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;
} }
@@ -125,7 +123,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;
} }
@@ -202,7 +200,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;
@@ -212,7 +210,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,7 +15,6 @@ 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;
@@ -30,7 +29,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
{ {
@@ -62,7 +61,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;
} }
@@ -79,7 +78,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;
} }
@@ -96,7 +95,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;
} }
@@ -117,7 +116,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,7 +12,6 @@ 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;
@@ -27,7 +26,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
{ {
@@ -95,7 +94,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,7 +13,6 @@ 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;
@@ -30,7 +29,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
{ {
@@ -106,7 +105,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,6 +19,7 @@ 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;
@@ -99,7 +100,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, Member, 'membership'> * @return BelongsToMany<User, $this, Pivot, 'membership'>
*/ */
public function users(): BelongsToMany public function users(): BelongsToMany
{ {
@@ -132,7 +133,7 @@ class Organization extends Model implements AuditableContract
} }
/** /**
* @return BelongsToMany<User, $this, Member, 'membership'> * @return BelongsToMany<User, $this, Pivot, 'membership'>
*/ */
public function realUsers(): BelongsToMany public function realUsers(): BelongsToMany
{ {

View File

@@ -20,6 +20,7 @@ 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;
@@ -174,7 +175,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
} }
/** /**
* @return BelongsToMany<Organization, $this, Member, 'membership'> * @return BelongsToMany<Organization, $this, Pivot, 'membership'>
*/ */
public function organizations(): BelongsToMany public function organizations(): BelongsToMany
{ {
@@ -189,7 +190,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
} }
/** /**
* @return BelongsToMany<Organization, $this, Member, 'membership'> * @return BelongsToMany<Organization, $this, Pivot, '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\Schemas\Components\Section; use Filament\Forms\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 App\Http\Middleware\AuthenticateFilamentPanel; use Filament\Http\Middleware\Authenticate;
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\Dashboard; use Filament\Pages;
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\PreventRequestForgery; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
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([
Dashboard::class, Pages\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,6 +51,7 @@ 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()) {
@@ -71,9 +72,6 @@ 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,
@@ -81,13 +79,13 @@ class AdminPanelProvider extends PanelProvider
StartSession::class, StartSession::class,
AuthenticateSession::class, AuthenticateSession::class,
ShareErrorsFromSession::class, ShareErrorsFromSession::class,
PreventRequestForgery::class, VerifyCsrfToken::class,
SubstituteBindings::class, SubstituteBindings::class,
DisableBladeIconComponents::class, DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class, DispatchServingFilamentEvent::class,
]) ])
->authMiddleware([ ->authMiddleware([
AuthenticateFilamentPanel::class, Authenticate::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,18 +15,11 @@ 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 {
@@ -37,7 +30,6 @@ 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'])) {
@@ -50,7 +42,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(),
]); ]);
@@ -69,7 +61,6 @@ 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,7 +16,6 @@ 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
{ {
@@ -102,7 +101,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;
@@ -152,13 +151,12 @@ 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')) // @phpstan-ignore argument.type ->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
->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.')')) // @phpstan-ignore argument.type ->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
->orderBy('date'); ->orderBy('date');
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
@@ -195,13 +193,12 @@ 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')) // @phpstan-ignore argument.type ->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
->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.')')) // @phpstan-ignore argument.type ->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
->orderBy('date'); ->orderBy('date');
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone); $query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);

View File

@@ -17,7 +17,6 @@ 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
{ {
@@ -102,15 +101,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;
@@ -146,7 +145,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) [
@@ -172,7 +171,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;
@@ -189,10 +188,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,7 +9,6 @@ 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
@@ -134,7 +133,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);
@@ -165,7 +164,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,14 +9,13 @@ 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 {
@@ -45,7 +44,7 @@ class ClockifyProjectsImporter extends DefaultImporter
'organization_id' => $this->organization->id, 'organization_id' => $this->organization->id,
], [ ], [
'color' => $this->colorService->getRandomColor(), 'color' => $this->colorService->getRandomColor(),
'is_billable' => ($record['Billability'] ?? '') === 'Yes', 'is_billable' => $record['Billability'] === 'Yes',
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null, 'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null, 'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null, 'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
@@ -55,7 +54,6 @@ class ClockifyProjectsImporter extends DefaultImporter
if ($tasksKey !== null && $record[$tasksKey] !== '') { if ($tasksKey !== null && $record[$tasksKey] !== '') {
$tasks = explode(', ', $record[$tasksKey]); $tasks = explode(', ', $record[$tasksKey]);
foreach ($tasks as $task) { foreach ($tasks as $task) {
$this->checkTaskNameLength($task);
$this->taskImportHelper->getKey([ $this->taskImportHelper->getKey([
'name' => $task, 'name' => $task,
'project_id' => $projectId, 'project_id' => $projectId,
@@ -85,6 +83,7 @@ class ClockifyProjectsImporter extends DefaultImporter
'Project', 'Project',
'Status', 'Status',
'Visibility', 'Visibility',
'Billability',
]; ];
foreach ($requiredFields as $requiredField) { foreach ($requiredFields as $requiredField) {
if (! in_array($requiredField, $header, true)) { if (! in_array($requiredField, $header, true)) {
@@ -130,13 +129,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,7 +15,6 @@ 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
{ {
@@ -45,7 +44,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 {
@@ -104,7 +103,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
} }
$taskId = null; $taskId = null;
if (! $isBreak && $taskKey !== null && $record[$taskKey] !== '') { if (! $isBreak && $taskKey !== null && $record[$taskKey] !== '') {
$this->checkTaskNameLength($record[$taskKey]);
$taskId = $this->taskImportHelper->getKey([ $taskId = $this->taskImportHelper->getKey([
'name' => $record[$taskKey], 'name' => $record[$taskKey],
'project_id' => $projectId, 'project_id' => $projectId,
@@ -264,13 +262,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,12 +18,9 @@ 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
{ {
protected const TASK_NAME_MAX_LENGTH = 500;
protected Organization $organization; protected Organization $organization;
/** /**
@@ -184,17 +181,7 @@ abstract class DefaultImporter implements ImporterContract
$this->billableRateService = app(BillableRateService::class); $this->billableRateService = app(BillableRateService::class);
} }
/** #[\Override]
* @throws ImportException
*/
protected function checkTaskNameLength(string $taskName): void
{
if (strlen($taskName) > self::TASK_NAME_MAX_LENGTH) {
throw new ImportException('Task name ("'.$taskName.'") is too long, maximum length is '.self::TASK_NAME_MAX_LENGTH.' characters');
}
}
#[Override]
public function getReport(): ReportDto public function getReport(): ReportDto
{ {
return new ReportDto( return new ReportDto(

View File

@@ -14,7 +14,6 @@ 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
{ {
@@ -60,7 +59,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 {
@@ -114,7 +113,6 @@ class GenericTimeEntriesImporter extends DefaultImporter
} }
$taskId = null; $taskId = null;
if ($record['task'] !== '') { if ($record['task'] !== '') {
$this->checkTaskNameLength($record['task']);
$taskId = $this->taskImportHelper->getKey([ $taskId = $this->taskImportHelper->getKey([
'name' => $record['task'], 'name' => $record['task'],
'project_id' => $projectId, 'project_id' => $projectId,
@@ -196,13 +194,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,7 +7,6 @@ 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
{ {
@@ -21,7 +20,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 {
@@ -63,13 +62,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,7 +8,6 @@ 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
{ {
@@ -25,7 +24,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 {
@@ -94,13 +93,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

@@ -92,7 +92,6 @@ class HarvestTimeEntriesImporter extends DefaultImporter
} }
$taskId = null; $taskId = null;
if ($record['Task'] !== '') { if ($record['Task'] !== '') {
$this->checkTaskNameLength($record['Task']);
$taskId = $this->taskImportHelper->getKey([ $taskId = $this->taskImportHelper->getKey([
'name' => $record['Task'], 'name' => $record['Task'],
'project_id' => $projectId, 'project_id' => $projectId,

View File

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

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Service\Import\Importers; namespace App\Service\Import\Importers;
use InvalidArgumentException;
class ImporterProvider class ImporterProvider
{ {
/** /**
@@ -51,7 +49,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

@@ -199,7 +199,6 @@ class SolidtimeImporter extends DefaultImporter
if ($projectId === null) { if ($projectId === null) {
throw new Exception('Project does not exist'); throw new Exception('Project does not exist');
} }
$this->checkTaskNameLength($task['name']);
$this->taskImportHelper->getKey([ $this->taskImportHelper->getKey([
'name' => $task['name'], 'name' => $task['name'],
'project_id' => $projectId, 'project_id' => $projectId,

View File

@@ -153,16 +153,9 @@ class TogglDataImporter extends DefaultImporter
} }
foreach ($projectMembers as $projectMember) { foreach ($projectMembers as $projectMember) {
$userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id); $userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
if ($userId === null) {
throw new Exception('User does not exist');
}
$memberId = $this->memberImportHelper->getKeyByExternalIdentifier($userId);
if ($memberId === null) {
throw new Exception('Member does not exist');
}
$this->projectMemberImportHelper->getKey([ $this->projectMemberImportHelper->getKey([
'project_id' => $projectId, 'project_id' => $projectId,
'member_id' => $memberId, 'member_id' => $this->memberImportHelper->getKeyByExternalIdentifier($userId),
], [ ], [
'user_id' => $userId, 'user_id' => $userId,
'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null, 'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
@@ -189,7 +182,6 @@ class TogglDataImporter extends DefaultImporter
if ($projectId === null) { if ($projectId === null) {
throw new Exception('Project does not exist'); throw new Exception('Project does not exist');
} }
$this->checkTaskNameLength($task->name);
$this->taskImportHelper->getKey([ $this->taskImportHelper->getKey([
'name' => $task->name, 'name' => $task->name,
'project_id' => $projectId, 'project_id' => $projectId,

View File

@@ -14,7 +14,6 @@ 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
{ {
@@ -44,7 +43,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 {
@@ -98,7 +97,6 @@ class TogglTimeEntriesImporter extends DefaultImporter
} }
$taskId = null; $taskId = null;
if ($record['Task'] !== '') { if ($record['Task'] !== '') {
$this->checkTaskNameLength($record['Task']);
$taskId = $this->taskImportHelper->getKey([ $taskId = $this->taskImportHelper->getKey([
'name' => $record['Task'], 'name' => $record['Task'],
'project_id' => $projectId, 'project_id' => $projectId,
@@ -194,13 +192,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,7 +14,6 @@ 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;
/** /**
@@ -117,7 +116,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,7 +20,6 @@ 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
{ {
@@ -75,9 +74,8 @@ 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,' : ''). // @phpstan-ignore argument.type ($group1Select !== null ? $group1Select.' as group_1,' : '').
($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'
@@ -106,7 +104,7 @@ class TimeEntryAggregationService
$baseTotalsPerGroup1Query = $baseTotalsQuery->clone(); $baseTotalsPerGroup1Query = $baseTotalsQuery->clone();
$baseTotalsPerGroup1 = $baseTotalsPerGroup1Query $baseTotalsPerGroup1 = $baseTotalsPerGroup1Query
->selectRaw( ->selectRaw(
$group1Select.' as group_1,'. // @phpstan-ignore argument.type $group1Select.' as group_1,'.
' 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'
) )
@@ -171,7 +169,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,'. // @phpstan-ignore argument.type ' 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'
) )
->first(); ->first();
@@ -528,7 +526,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);
@@ -541,7 +539,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,7 +6,6 @@ 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;
@@ -288,7 +287,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