Compare commits

..

1 Commits

Author SHA1 Message Date
Constantin Graf
7a83d96b79 Remove Group from ClockifyTimeEntriesImporter 2026-09-22 18:02:46 +02:00
220 changed files with 2502 additions and 12850 deletions

View File

@@ -6,7 +6,6 @@ APP_DEBUG=true
APP_URL=http://localhost
APP_FORCE_HTTPS=false
APP_ENABLE_REGISTRATION=true
SUPER_ADMINS=admin@example.com
# Logging
LOG_CHANNEL=stack
@@ -65,6 +64,3 @@ GOTENBERG_URL=http://localhost:3000
# Octane
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_APP_NAME="${APP_NAME}"
#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
DB_CONNECTION=pgsql
INERTIA_SSR_ENABLED=false

View File

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

View File

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

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,8 +1,6 @@
name: NPM Format Check
on: [push]
permissions:
contents: read
jobs:
format-check:

2
.gitignore vendored
View File

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

View File

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

View File

@@ -10,7 +10,6 @@ use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use stdClass;
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
{

View File

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

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum GoalComparison: string
{
use LaravelEnumHelper;
/**
* The goal is reached when at least the target amount of time was tracked in the period.
*/
case AtLeast = 'at_least';
/**
* The goal is reached when less than the target amount of time was tracked in the period.
*/
case LessThan = 'less_than';
}

View File

@@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum GoalPeriod: string
{
use LaravelEnumHelper;
case Day = 'day';
case Week = 'week';
case Month = 'month';
}

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum GoalStatus: string
{
use LaravelEnumHelper;
/**
* "At least" goal, the target has not been reached yet in the current period.
*/
case InProgress = 'in_progress';
/**
* "At least" goal, the target has been reached in the current period.
*/
case Achieved = 'achieved';
/**
* "Less than" goal, the tracked time is still below the limit.
*/
case OnTrack = 'on_track';
/**
* "Less than" goal, the tracked time reached or exceeded the limit.
*/
case Exceeded = 'exceeded';
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
enum GoalType: string
{
use LaravelEnumHelper;
/**
* A goal a member set for themselves. Only that member can see and manage it, no permission reaches it.
*/
case Personal = 'personal';
/**
* A goal the organization set for a member or for the whole organization.
* Managed by the members that may manage goals, visible to them and to the member the goal is for.
* Requires the team goals extension.
*/
case Organization = 'organization';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListClients extends ListRecords
@@ -15,7 +15,7 @@ class ListClients extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->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\ViewFailedJobs;
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\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
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\Table;
use Illuminate\Support\Collection;
@@ -30,19 +30,19 @@ class FailedJobResource extends Resource
{
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
{
return (string) FailedJob::query()->count();
}
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('uuid')->disabled()->columnSpan(4),
TextInput::make('failed_at')->disabled(),
TextInput::make('id')->disabled(),
@@ -74,7 +74,7 @@ class FailedJobResource extends Resource
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
])
->filters([])
->toolbarActions([
->bulkActions([
BulkAction::make('retry')
->icon('heroicon-o-arrow-path')
->label('Retry selected')
@@ -91,7 +91,7 @@ class FailedJobResource extends Resource
}),
DeleteBulkAction::make(),
])
->recordActions([
->actions([
DeleteAction::make(),
ViewAction::make(),
Action::make('retry')

View File

@@ -1,173 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Enums\GoalComparison;
use App\Enums\GoalPeriod;
use App\Enums\GoalType;
use App\Enums\Weekday;
use App\Filament\Resources\GoalResource\Pages;
use App\Models\Goal;
use App\Service\Dto\GoalFiltersDto;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms;
use Filament\Forms\Components\DateTimePicker;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
class GoalResource extends Resource
{
protected static ?string $model = Goal::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-viewfinder-circle';
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
protected static ?int $navigationSort = 8;
public static function form(Schema $schema): Schema
{
return $schema
->columns(1)
->components([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Forms\Components\Select::make('type')
->label('Type')
->options(collect(GoalType::cases())->mapWithKeys(fn (GoalType $type): array => [$type->value => $type->name])->all())
->disabled()
->required(),
Forms\Components\Select::make('comparison')
->label('Comparison')
->options(collect(GoalComparison::cases())->mapWithKeys(fn (GoalComparison $comparison): array => [$comparison->value => $comparison->name])->all())
->required(),
Forms\Components\TextInput::make('target_seconds')
->label('Target (seconds)')
->numeric()
->minValue(1)
->required(),
Forms\Components\Select::make('period')
->label('Period')
->options(collect(GoalPeriod::cases())->mapWithKeys(fn (GoalPeriod $period): array => [$period->value => $period->name])->all())
->required(),
Forms\Components\Select::make('timezone')
->label('Timezone')
->options(fn (): array => collect(\DateTimeZone::listIdentifiers())->mapWithKeys(fn (string $timezone): array => [$timezone => $timezone])->all())
->searchable()
->required(),
Forms\Components\Select::make('week_start')
->label('Week start')
->options(collect(Weekday::cases())->mapWithKeys(fn (Weekday $weekday): array => [$weekday->value => $weekday->name])->all())
->required(),
DateTimePicker::make('archived_at')
->label('Archived At'),
Forms\Components\Select::make('organization_id')
->label('Organization')
->relationship(name: 'organization', titleAttribute: 'name')
->searchable(['name'])
->disabled()
->required(),
Forms\Components\Select::make('member_id')
->label('Member (empty = every member)')
->relationship(name: 'member', titleAttribute: 'id')
->disabled(),
PrettyJsonField::make('filters')
->formatStateUsing(function (GoalFiltersDto $state, Goal $record): string {
return $record->getRawOriginal('filters');
})
->disabled(),
DateTimePicker::make('created_at')
->label('Created At')
->hiddenOn(['create'])
->disabled(),
DateTimePicker::make('updated_at')
->label('Updated At')
->hiddenOn(['create'])
->disabled(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('comparison')
->sortable(),
TextColumn::make('target_seconds')
->label('Target (seconds)')
->sortable(),
TextColumn::make('period')
->sortable(),
TextColumn::make('type')
->sortable(),
TextColumn::make('member.user.email')
->label('Member')
->placeholder('Every member')
->searchable(),
TextColumn::make('archived_at')
->dateTime()
->placeholder('Not archived')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('organization.name')
->searchable()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('created_at', 'desc')
->filters([
SelectFilter::make('type')
->label('Type')
->options(collect(GoalType::cases())->mapWithKeys(fn (GoalType $type): array => [$type->value => $type->name])->all()),
SelectFilter::make('organization')
->label('Organization')
->relationship('organization', 'name')
->searchable(),
SelectFilter::make('organization_id')
->label('Organization ID')
->relationship('organization', 'id')
->searchable(),
])
->recordActions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
]);
}
public static function getRelations(): array
{
return [
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListGoals::route('/'),
'edit' => Pages\EditGoal::route('/{record}/edit'),
'view' => Pages\ViewGoal::route('/{record}'),
];
}
}

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\GoalResource\Pages;
use App\Filament\Resources\GoalResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditGoal extends EditRecord
{
protected static string $resource = GoalResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}
}

View File

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

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\GoalResource\Pages;
use App\Filament\Resources\GoalResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewGoal extends ViewRecord
{
protected static string $resource = GoalResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make('edit')
->icon('heroicon-s-pencil'),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,15 +12,14 @@ use App\Models\Organization;
use App\Models\User;
use App\Service\BillableRateService;
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\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
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\Table;
use Illuminate\Validation\Rule;
@@ -29,10 +28,10 @@ class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
public function form(Schema $schema): Schema
public function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
Select::make('role')
->options(Role::class),
TextInput::make('billable_rate')
@@ -76,17 +75,17 @@ class UsersRelationManager extends RelationManager
->using(function (User $record, array $data): void {
/** @var Organization $organization */
$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')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (User $record): string => UserResource::getUrl('view', [
'record' => $record->getKey(),
])),
EditAction::make()
Tables\Actions\EditAction::make()
->using(function (User $record, array $data): User {
/** @var Organization $organization */
$organization = $this->getOwnerRecord();
@@ -98,10 +97,9 @@ class UsersRelationManager extends RelationManager
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
}
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
if ($newRole->value !== $member->role) {
if ($data['role'] !== $member->role) {
try {
app(MemberService::class)->changeRole($member, $organization, $newRole, true);
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
} catch (ApiException $exception) {
Notification::make()
->danger()
@@ -115,7 +113,7 @@ class UsersRelationManager extends RelationManager
return $record;
}),
DetachAction::make()
Tables\Actions\DetachAction::make()
->using(function (User $record): void {
/** @var Organization $organization */
$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;
use App\Filament\Resources\ProjectMemberResource\Pages\CreateProjectMember;
use App\Filament\Resources\ProjectMemberResource\Pages\EditProjectMember;
use App\Filament\Resources\ProjectMemberResource\Pages\ListProjectMembers;
use App\Filament\Resources\ProjectMemberResource\Pages\ViewProjectMembers;
use App\Filament\Resources\ProjectMemberResource\Pages;
use App\Models\ProjectMember;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables;
use Filament\Tables\Table;
class ProjectMemberResource extends Resource
@@ -25,11 +18,11 @@ class ProjectMemberResource extends Resource
protected static bool $shouldRegisterNavigation = false;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
TextInput::make('billable_rate')
return $form
->schema([
Forms\Components\TextInput::make('billable_rate')
->label('Billable rate (in Cents)')
->nullable()
->rules([
@@ -39,10 +32,10 @@ class ProjectMemberResource extends Resource
'max:2147483647',
])
->numeric(),
Select::make('user_id')
Forms\Components\Select::make('user_id')
->relationship('user', 'name')
->required(),
Select::make('member_id')
Forms\Components\Select::make('member_id')
->relationship('member', 'id')
->required(),
]);
@@ -52,17 +45,17 @@ class ProjectMemberResource extends Resource
{
return $table
->columns([
TextColumn::make('id')
Tables\Columns\TextColumn::make('id')
->label('ID'),
TextColumn::make('billable_rate')
Tables\Columns\TextColumn::make('billable_rate')
->numeric()
->sortable(),
TextColumn::make('project.name'),
TextColumn::make('user.name'),
TextColumn::make('created_at')
Tables\Columns\TextColumn::make('project.name'),
Tables\Columns\TextColumn::make('user.name'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
TextColumn::make('updated_at')
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
@@ -70,12 +63,12 @@ class ProjectMemberResource extends Resource
->filters([
//
])
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -90,10 +83,10 @@ class ProjectMemberResource extends Resource
public static function getPages(): array
{
return [
'index' => ListProjectMembers::route('/'),
'create' => CreateProjectMember::route('/create'),
'edit' => EditProjectMember::route('/{record}/edit'),
'view' => ViewProjectMembers::route('/{record}'),
'index' => Pages\ListProjectMembers::route('/'),
'create' => Pages\CreateProjectMember::route('/create'),
'edit' => Pages\EditProjectMember::route('/{record}/edit'),
'view' => Pages\ViewProjectMembers::route('/{record}'),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,20 +4,16 @@ declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TimeEntryResource\Pages\CreateTimeEntry;
use App\Filament\Resources\TimeEntryResource\Pages\EditTimeEntry;
use App\Filament\Resources\TimeEntryResource\Pages\ListTimeEntries;
use App\Filament\Resources\TimeEntryResource\Pages;
use App\Models\Member;
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\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
@@ -27,16 +23,16 @@ class TimeEntryResource extends Resource
{
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;
public static function form(Schema $schema): Schema
public static function form(Form $form): Form
{
return $schema
->components([
return $form
->schema([
TextInput::make('id')
->label('ID')
->readOnly()
@@ -115,12 +111,12 @@ class TimeEntryResource extends Resource
->searchable(),
])
->defaultSort('created_at', 'desc')
->recordActions([
EditAction::make(),
->actions([
Tables\Actions\EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
@@ -135,9 +131,9 @@ class TimeEntryResource extends Resource
public static function getPages(): array
{
return [
'index' => ListTimeEntries::route('/'),
'create' => CreateTimeEntry::route('/create'),
'edit' => EditTimeEntry::route('/{record}/edit'),
'index' => Pages\ListTimeEntries::route('/'),
'create' => Pages\CreateTimeEntry::route('/create'),
'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\Models\Member;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTimeEntry extends EditRecord
@@ -16,7 +16,7 @@ class EditTimeEntry extends EditRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
Actions\DeleteAction::make()
->icon('heroicon-m-trash'),
];
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,8 @@ declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Filament\Resources\UserResource\Actions\DeleteUser;
use Filament\Resources\Pages\EditRecord;
use STS\FilamentImpersonate\Actions\Impersonate;
use STS\FilamentImpersonate\Pages\Actions\Impersonate;
class EditUser extends EditRecord
{
@@ -17,7 +16,7 @@ class EditUser extends EditRecord
{
return [
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;
use App\Filament\Resources\UserResource;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
@@ -15,7 +15,7 @@ class ListUsers extends ListRecords
protected function getHeaderActions(): array
{
return [
CreateAction::make()
Actions\CreateAction::make()
->icon('heroicon-s-plus'),
];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
class UserRegistrations extends ChartWidget
{
protected ?string $heading = 'User Registrations';
protected static ?string $heading = 'User Registrations';
public ?string $filter = 'week';
@@ -54,7 +54,7 @@ class UserRegistrations extends ChartWidget
return [
'datasets' => [
[
'label' => $this->heading,
'label' => self::$heading,
'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\Http\JsonResponse;
use Illuminate\Support\Str;
use RuntimeException;
class ApiTokenController extends Controller
{
@@ -63,7 +62,7 @@ class ApiTokenController extends Controller
$tokenModel = $token->getToken();
return new ApiTokenWithAccessTokenResource($tokenModel, $token->accessToken);
} catch (RuntimeException $exception) {
} catch (\RuntimeException $exception) {
report($exception);
if (Str::contains($exception->getMessage(), ['Personal access client not found'])) {
throw new PersonalAccessClientIsNotConfiguredException;

View File

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

View File

@@ -1,243 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\GoalType;
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
use App\Http\Requests\V1\Goal\GoalIndexRequest;
use App\Http\Requests\V1\Goal\GoalStoreRequest;
use App\Http\Requests\V1\Goal\GoalUpdateRequest;
use App\Http\Resources\V1\Goal\GoalCollection;
use App\Http\Resources\V1\Goal\GoalResource;
use App\Models\Goal;
use App\Models\Organization;
use App\Service\BillingContract;
use App\Service\GoalProgressService;
use App\Service\GoalsContract;
use App\Service\TimezoneService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;
class GoalController extends Controller
{
/**
* @throws AuthorizationException
*/
protected function checkPermission(Organization $organization, string $permission, ?Goal $goal = null): void
{
parent::checkPermission($organization, $permission);
if ($goal !== null) {
$this->checkGoalBelongsToOrganization($organization, $goal);
}
}
/**
* @throws AuthorizationException
*/
private function checkGoalBelongsToOrganization(Organization $organization, Goal $goal): void
{
if ($goal->organization_id !== $organization->id) {
throw new AuthorizationException('Goal does not belong to organization');
}
}
/**
* Enforces the goal limit provided by the BillingContract, if any. Archived goals count toward the limit.
*
* @throws FeatureIsNotAvailableInFreePlanApiException
*/
private function checkGoalLimit(Organization $organization): void
{
$limit = app(BillingContract::class)->getGoalLimit($organization);
if ($limit === null) {
return;
}
$goals = Goal::query()
->whereBelongsTo($organization, 'organization')
->count();
if ($goals >= $limit) {
throw new FeatureIsNotAvailableInFreePlanApiException;
}
}
/**
* Get goals
*
* Returns the goals the current member is allowed to see, including the progress in the current period.
*
* @return GoalCollection<GoalResource>
*
* @throws AuthorizationException
*
* @operationId getGoals
*/
public function index(Organization $organization, GoalIndexRequest $request, GoalsContract $access, GoalProgressService $progressService): GoalCollection
{
// An organization goal is visible to the member it is for, who does not hold the :organization-type permission.
// Either permission is enough, the access contract decides which goals come back.
$this->checkAnyPermission($organization, ['goals:view:own', 'goals:view:organization-type']);
$member = $this->member($organization);
$query = Goal::query()
->whereBelongsTo($organization, 'organization')
->with(['member.user'])
->orderBy('created_at', 'desc')
->orderBy('id');
$query = $access->scopeVisibleGoals($query, $member);
if ($request->getType() !== null) {
$query->where('type', '=', $request->getType()->value);
}
if ($request->getArchivedFilter() === 'true') {
$query->archived();
} elseif ($request->getArchivedFilter() === 'false') {
$query->notArchived();
}
$goals = $query->paginate(config('app.pagination_per_page_default'));
$progressByGoalId = $progressService->getCurrentProgressForGoals($goals->getCollection(), Carbon::now());
return new GoalCollection($goals, $progressByGoalId);
}
/**
* Get goal
*
* @throws AuthorizationException
*
* @operationId getGoal
*/
public function show(Organization $organization, Goal $goal, GoalsContract $access, GoalProgressService $progressService): GoalResource
{
// Either permission is enough, see index
$this->checkAnyPermission($organization, ['goals:view:own', 'goals:view:organization-type']);
$this->checkGoalBelongsToOrganization($organization, $goal);
$member = $this->member($organization);
if (! $access->canViewGoal($member, $goal)) {
throw new AuthorizationException;
}
$goal->load('member.user');
return new GoalResource($goal, $progressService->getCurrentProgress($goal, Carbon::now()));
}
/**
* Create goal
*
* @throws AuthorizationException|FeatureIsNotAvailableInFreePlanApiException
*
* @operationId createGoal
*/
public function store(Organization $organization, GoalStoreRequest $request, GoalsContract $access, GoalProgressService $progressService): JsonResponse
{
if ($request->getType() === GoalType::Personal) {
$this->checkPermission($organization, 'goals:create:own');
} else {
$this->checkPermission($organization, 'goals:create:organization-type');
}
$member = $this->member($organization);
$type = $request->getType();
$targetMemberId = $request->hasMemberId() ? $request->getMemberId() : $member->getKey();
if (! $access->canCreateGoal($member, $type, $targetMemberId)) {
throw new AuthorizationException;
}
$this->checkGoalLimit($organization);
$user = $member->user;
$goal = new Goal;
$goal->name = $request->getName();
$goal->type = $type;
$goal->comparison = $request->getComparison();
$goal->target_seconds = $request->getTargetSeconds();
$goal->period = $request->getPeriod();
$goal->filters = $request->getFilters();
$goal->timezone = $request->getTimezone() ?? app(TimezoneService::class)->getTimezoneFromUser($user)->getName();
$goal->week_start = $request->getWeekStart() ?? $user->week_start;
$goal->organization()->associate($organization);
$goal->member_id = $targetMemberId;
$goal->save();
$goal->load('member.user');
return (new GoalResource($goal, $progressService->getCurrentProgress($goal, Carbon::now())))
->response()
->setStatusCode(201);
}
/**
* Update goal
*
* The type of a goal and the member it is for can not be changed after creation.
*
* @throws AuthorizationException
*
* @operationId updateGoal
*/
public function update(Organization $organization, Goal $goal, GoalUpdateRequest $request, GoalsContract $access, GoalProgressService $progressService): GoalResource
{
if ($goal->type === GoalType::Personal) {
$this->checkPermission($organization, 'goals:update:own', $goal);
} else {
$this->checkPermission($organization, 'goals:update:organization-type', $goal);
}
$member = $this->member($organization);
if (! $access->canUpdateGoal($member, $goal)) {
throw new AuthorizationException;
}
if ($request->has('filters')) {
$goal->filters = $request->getFilters();
}
if ($request->has('name')) {
$goal->name = $request->getName();
}
if ($request->has('comparison')) {
$goal->comparison = $request->getComparison();
}
if ($request->has('target_seconds')) {
$goal->target_seconds = $request->getTargetSeconds();
}
if ($request->has('period')) {
$goal->period = $request->getPeriod();
}
if ($request->has('timezone')) {
$goal->timezone = $request->getTimezone();
}
if ($request->has('week_start')) {
$goal->week_start = $request->getWeekStart();
}
if ($request->has('is_archived')) {
$goal->archived_at = $request->getIsArchived() ? Carbon::now() : null;
}
$goal->save();
$goal->load('member.user');
return new GoalResource($goal, $progressService->getCurrentProgress($goal, Carbon::now()));
}
/**
* Delete goal
*
* @throws AuthorizationException
*
* @operationId deleteGoal
*/
public function destroy(Organization $organization, Goal $goal, GoalsContract $access): JsonResponse
{
if ($goal->type === GoalType::Personal) {
$this->checkPermission($organization, 'goals:delete:own', $goal);
} else {
$this->checkPermission($organization, 'goals:delete:organization-type', $goal);
}
$member = $this->member($organization);
if (! $access->canDeleteGoal($member, $goal)) {
throw new AuthorizationException;
}
$goal->delete();
return response()->json(null, 204);
}
}

View File

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

View File

@@ -55,7 +55,6 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use LogicException;
use Maatwebsite\Excel\Facades\Excel;
use Spatie\TemporaryDirectory\TemporaryDirectory;
@@ -191,9 +190,8 @@ class TimeEntryController extends Controller
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
if ($roundingType !== null && $roundingMinutes !== null) {
$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'); // @phpstan-ignore argument.type
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); // @phpstan-ignore argument.type
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end');
}
$timeEntriesQuery = TimeEntry::query()
->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'));
if ($viewFile === false) {
throw new LogicException('View file not found');
throw new \LogicException('View file not found');
}
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $memberFilter);
$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'));
if ($footerViewFile === false) {
throw new LogicException('View file not found');
throw new \LogicException('View file not found');
}
$footerHtml = Blade::render($footerViewFile);
if ($debug) {
@@ -497,7 +495,7 @@ class TimeEntryController extends Controller
]);
$viewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf.blade.php'));
if ($viewFile === false) {
throw new LogicException('View file not found');
throw new \LogicException('View file not found');
}
$html = Blade::render($viewFile, [
'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'));
if ($footerViewFile === false) {
throw new LogicException('View file not found');
throw new \LogicException('View file not found');
}
$footerHtml = Blade::render($footerViewFile);
if ($debug) {

View File

@@ -12,7 +12,7 @@ class TimeZoneController extends Controller
/**
* Get all timezones
*
* @response array{key: string}[]
* @response object{key: string}[]
*
* @operationId getTimezones
*/

View File

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

View File

@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace App\Http\Controllers\Web;
use App\Enums\Weekday;
use App\Service\Dto\UserAgentDto;
use App\Service\TimezoneService;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Carbon;
@@ -14,7 +16,6 @@ use Inertia\Inertia;
use Inertia\Response;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Features;
use stdClass;
class UserProfileController extends Controller
{
@@ -92,6 +93,8 @@ class UserProfileController extends Controller
$this->validateTwoFactorAuthenticationState($request);
return Inertia::render('Profile/Show', [
'timezones' => app(TimezoneService::class)->getSelectOptions(),
'weekdays' => Weekday::toSelectArray(),
'confirmsTwoFactorAuthentication' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
'sessions' => $this->sessions($request),
]);
@@ -100,7 +103,7 @@ class UserProfileController extends Controller
/**
* 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
{

View File

@@ -11,7 +11,6 @@ use App\Http\Middleware\EnsureEmailIsVerified;
use App\Http\Middleware\ForceHttps;
use App\Http\Middleware\ForceJsonResponse;
use App\Http\Middleware\HandleInertiaRequests;
use App\Http\Middleware\PreventRequestForgery;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\ShareInertiaData;
@@ -19,6 +18,7 @@ use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustHosts;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\RequirePassword;
@@ -68,7 +68,7 @@ class Kernel extends HttpKernel
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
PreventRequestForgery::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
HandleInertiaRequests::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 Closure;
use Illuminate\Http\Request;
use LogicException;
use Symfony\Component\HttpFoundation\Response;
class CheckOrganizationBlocked
@@ -26,7 +25,7 @@ class CheckOrganizationBlocked
$organization = $request->route('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 */

View File

@@ -42,7 +42,6 @@ class HandleInertiaRequests extends Middleware
$hasBilling = Module::has('Billing') && Module::isEnabled('Billing');
$hasInvoicing = Module::has('Invoicing') && Module::isEnabled('Invoicing');
$hasServices = Module::has('Services') && Module::isEnabled('Services');
$hasGoals = Module::has('Goals') && Module::isEnabled('Goals');
/** @var BillingContract $billing */
$billing = app(BillingContract::class);
@@ -53,13 +52,11 @@ class HandleInertiaRequests extends Middleware
'has_billing_extension' => $hasBilling,
'has_invoicing_extension' => $hasInvoicing,
'has_services_extension' => $hasServices,
'has_goals_extension' => $hasGoals,
'billing' => $currentOrganization !== null ? [
'has_subscription' => $billing->hasSubscription($currentOrganization),
'has_trial' => $billing->hasTrial($currentOrganization),
'trial_until' => $billing->getTrialUntil($currentOrganization)?->toIso8601ZuluString(),
'is_blocked' => $billing->isBlocked($currentOrganization),
'goal_limit' => $billing->getGoalLimit($currentOrganization),
] : null,
'flash' => [
'message' => fn () => $request->session()->get('message'),

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;
use Closure;
use Illuminate\Http\Middleware\TrustHosts as BaseTrustHosts;
use Illuminate\Http\Request;
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)
{

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

@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Goal;
use App\Enums\Role;
use App\Enums\TagMatchType;
use App\Enums\TimeEntryType;
use App\Models\Client;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Project;
use App\Models\Tag;
use App\Models\Task;
use App\Service\Dto\GoalFiltersDto;
use App\Service\TimeEntryFilter;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rule;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
/**
* Shared validation rules for the filters of a goal.
*
* @property Organization $organization Organization from model binding
*/
trait GoalFilterRules
{
/**
* @param bool $forEveryMember Whether the goal counts every member instead of one member
* @return array<string, array<string|ValidationRule|LegacyValidationRule|\Closure>>
*/
protected function filterRules(bool $forEveryMember): array
{
return [
'filters' => [
'sometimes',
'array',
],
// Filter by member IDs, member IDs are OR combined. Only for goals that count every member, null means every member.
'filters.member_ids' => [
'nullable',
'array',
// "prohibited" still lets null and an empty array through, both mean "no restriction"
...($forEveryMember ? [] : ['prohibited']),
],
'filters.member_ids.*' => [
'string',
'distinct',
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization')
->where('role', '!=', Role::Placeholder->value);
})->uuid(),
],
// Filter by project IDs, project IDs are OR combined, "none" matches entries without a project
'filters.project_ids' => [
'nullable',
'array',
],
'filters.project_ids.*' => [
'string',
$this->idOrNoneExistsInOrganization(Project::class),
],
// Filter by task IDs, task IDs are OR combined, "none" matches entries without a task
'filters.task_ids' => [
'nullable',
'array',
],
'filters.task_ids.*' => [
'string',
$this->idOrNoneExistsInOrganization(Task::class),
],
// Filter by tag IDs, tag IDs are OR combined, "none" matches entries without tags
'filters.tag_ids' => [
'nullable',
'array',
],
'filters.tag_ids.*' => [
'string',
$this->idOrNoneExistsInOrganization(Tag::class),
],
'filters.tag_match_type' => [
'nullable',
'string',
Rule::enum(TagMatchType::class),
],
// Filter by client IDs, client IDs are OR combined, "none" matches entries without a client
'filters.client_ids' => [
'nullable',
'array',
],
'filters.client_ids.*' => [
'string',
$this->idOrNoneExistsInOrganization(Client::class),
],
// Filter by billable status, null means both
'filters.billable' => [
'nullable',
'boolean',
],
// Filter by time entry type, null means both
'filters.time_entry_type' => [
'nullable',
'string',
Rule::enum(TimeEntryType::class),
],
];
}
/**
* @param class-string<Project|Task|Tag|Client> $modelClass
*/
private function idOrNoneExistsInOrganization(string $modelClass): \Closure
{
return function (string $attribute, mixed $value, \Closure $fail) use ($modelClass): void {
if ($value === TimeEntryFilter::NONE_VALUE) {
return;
}
ExistsEloquent::make($modelClass, null, function (Builder $builder): Builder {
/** @var Builder<Project|Task|Tag|Client> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
})->uuid()->validate($attribute, $value, $fail);
};
}
public function getFilters(): GoalFiltersDto
{
$filters = new GoalFiltersDto;
$filters->setMemberIds($this->input('filters.member_ids'));
$filters->setProjectIds($this->input('filters.project_ids'));
$filters->setTaskIds($this->input('filters.task_ids'));
$filters->setTagIds($this->input('filters.tag_ids'));
$filters->tagMatchType = $this->input('filters.tag_match_type') !== null ? TagMatchType::from($this->input('filters.tag_match_type')) : null;
$filters->setClientIds($this->input('filters.client_ids'));
$filters->billable = $this->input('filters.billable') !== null ? (bool) $this->input('filters.billable') : null;
$filters->timeEntryType = $this->input('filters.time_entry_type') !== null ? TimeEntryType::from($this->input('filters.time_entry_type')) : null;
return $filters;
}
}

View File

@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Goal;
use App\Enums\GoalType;
use App\Http\Requests\V1\BaseFormRequest;
use App\Models\Organization;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rule;
/**
* @property Organization $organization Organization from model binding
*/
class GoalIndexRequest extends BaseFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule|LegacyValidationRule|\Closure>>
*/
public function rules(): array
{
return [
// Only return goals of this type (personal, organization)
'type' => [
'nullable',
'string',
Rule::enum(GoalType::class),
],
// Filter by archived status, "true" only archived, "false" only not archived (default), "all" both
'archived' => [
'nullable',
'string',
'in:true,false,all',
],
'page' => [
'nullable',
'integer',
'min:1',
'max:2147483647',
],
];
}
public function getType(): ?GoalType
{
if ($this->input('type') === null) {
return null;
}
return GoalType::from($this->input('type'));
}
public function getArchivedFilter(): string
{
return (string) $this->input('archived', 'false');
}
}

View File

@@ -1,167 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Goal;
use App\Enums\GoalComparison;
use App\Enums\GoalPeriod;
use App\Enums\GoalType;
use App\Enums\Role;
use App\Enums\Weekday;
use App\Http\Requests\V1\BaseFormRequest;
use App\Models\Member;
use App\Models\Organization;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
/**
* @property Organization $organization Organization from model binding
*/
class GoalStoreRequest extends BaseFormRequest
{
use GoalFilterRules;
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule|LegacyValidationRule|In|\Closure>>
*/
public function rules(): array
{
$memberExistsInOrganization = ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
/** @var Builder<Member> $builder */
return $builder->whereBelongsTo($this->organization, 'organization')
->where('role', '!=', Role::Placeholder->value);
})->uuid();
return array_merge([
'name' => [
'required',
'string',
'max:255',
],
// personal: a goal for yourself that nobody else can see, organization: a goal of the organization for a member or for every member. Organization goals require the team goals extension.
'type' => [
'required',
'string',
Rule::enum(GoalType::class),
],
// Whether the goal is reached when "at least" or "less than" the target time is tracked in the period
'comparison' => [
'required',
'string',
Rule::enum(GoalComparison::class),
],
// Target time in seconds
'target_seconds' => [
'required',
'integer',
'min:1',
'max:2147483647',
],
// The recurring time frame in which the target has to be reached
'period' => [
'required',
'string',
Rule::enum(GoalPeriod::class),
],
// ID of the member whose time entries count towards the goal, defaults to the current member. Send null for a goal that counts every member (organization goals only).
'member_id' => $this->input('type') === GoalType::Personal->value
// A personal goal is always for the current member, it can not count every member
? [
'sometimes',
'filled',
'string',
Rule::in([$this->currentMemberId()]),
]
: [
'nullable',
'string',
$memberExistsInOrganization,
],
// Timezone that defines the periods of the goal, defaults to the timezone of the current user
'timezone' => [
'nullable',
'timezone:all',
],
// Week start that defines weekly periods, defaults to the week start of the current user
'week_start' => [
'nullable',
'string',
Rule::enum(Weekday::class),
],
], $this->filterRules(
// Without a member_id the goal is for the current member
$this->has('member_id') && $this->input('member_id') === null,
));
}
private function currentMemberId(): ?string
{
/** @var string|null $memberId */
$memberId = Member::query()
->whereBelongsTo($this->organization, 'organization')
->where('user_id', $this->user()?->getKey())
->value('id');
return $memberId;
}
public function getName(): string
{
return (string) $this->input('name');
}
public function getType(): GoalType
{
return GoalType::from($this->input('type'));
}
public function getComparison(): GoalComparison
{
return GoalComparison::from($this->input('comparison'));
}
public function getTargetSeconds(): int
{
return (int) $this->input('target_seconds');
}
public function getPeriod(): GoalPeriod
{
return GoalPeriod::from($this->input('period'));
}
public function hasMemberId(): bool
{
return $this->has('member_id');
}
public function getMemberId(): ?string
{
return $this->input('member_id');
}
public function getTimezone(): ?string
{
if (! $this->has('timezone') || $this->input('timezone') === null) {
return null;
}
return (string) $this->input('timezone');
}
public function getWeekStart(): ?Weekday
{
if (! $this->has('week_start') || $this->input('week_start') === null) {
return null;
}
return Weekday::from($this->input('week_start'));
}
}

View File

@@ -1,129 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Goal;
use App\Enums\GoalComparison;
use App\Enums\GoalPeriod;
use App\Enums\Weekday;
use App\Http\Requests\V1\BaseFormRequest;
use App\Models\Goal;
use App\Models\Organization;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rule;
/**
* @property Organization $organization Organization from model binding
*/
class GoalUpdateRequest extends BaseFormRequest
{
use GoalFilterRules;
/**
* Get the validation rules that apply to the request.
* The type of a goal and the member it is for can not be changed after creation.
*
* @return array<string, array<string|ValidationRule|LegacyValidationRule|\Closure>>
*/
public function rules(): array
{
/** @var Goal $goal */
$goal = $this->route('goal');
return array_merge([
'name' => [
'sometimes',
'string',
'max:255',
],
// Whether the goal is reached when "at least" or "less than" the target time is tracked in the period
'comparison' => [
'sometimes',
'string',
Rule::enum(GoalComparison::class),
],
// Target time in seconds
'target_seconds' => [
'sometimes',
'integer',
'min:1',
'max:2147483647',
],
// The recurring time frame in which the target has to be reached
'period' => [
'sometimes',
'string',
Rule::enum(GoalPeriod::class),
],
// Timezone that defines the periods of the goal
'timezone' => [
'sometimes',
'timezone:all',
],
// Week start that defines weekly periods
'week_start' => [
'sometimes',
'string',
Rule::enum(Weekday::class),
],
// Archived goals are hidden from the goal list by default, their progress is still calculated
'is_archived' => [
'sometimes',
'boolean',
],
// Only allowed if it matches the current type of the goal
'type' => [
function (string $attribute, mixed $value, \Closure $fail) use ($goal): void {
if ($value !== $goal->type->value) {
$fail('The '.$attribute.' of a goal can not be changed.');
}
},
],
// Only allowed if it matches the current member of the goal
'member_id' => [
function (string $attribute, mixed $value, \Closure $fail) use ($goal): void {
if ($value !== $goal->member_id) {
$fail('The '.$attribute.' of a goal can not be changed.');
}
},
],
], $this->filterRules($goal->member_id === null));
}
public function getName(): string
{
return (string) $this->input('name');
}
public function getComparison(): GoalComparison
{
return GoalComparison::from($this->input('comparison'));
}
public function getTargetSeconds(): int
{
return (int) $this->input('target_seconds');
}
public function getPeriod(): GoalPeriod
{
return GoalPeriod::from($this->input('period'));
}
public function getTimezone(): string
{
return (string) $this->input('timezone');
}
public function getWeekStart(): Weekday
{
return Weekday::from($this->input('week_start'));
}
public function getIsArchived(): bool
{
return (bool) $this->input('is_archived');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ use App\Models\Project;
use App\Models\Tag;
use App\Models\Task;
use App\Service\PermissionStore;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
@@ -27,7 +26,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
/**
* 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
{
@@ -95,7 +94,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
// Type of the time entry (work time or a break)
'type' => [
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) {
$fail('Breaks are disabled for this organization.');
}

View File

@@ -13,7 +13,6 @@ use App\Models\Tag;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Service\PermissionStore;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
@@ -30,7 +29,7 @@ class TimeEntryUpdateRequest extends BaseFormRequest
/**
* 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
{
@@ -106,7 +105,7 @@ class TimeEntryUpdateRequest extends BaseFormRequest
// Type of the time entry (work time or a break)
'type' => [
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
// breaks, but converting a work entry to a break is not allowed.
if ($value === TimeEntryType::Break->value

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Goal;
use App\Http\Resources\PaginatedResourceCollection;
use App\Models\Goal;
use App\Service\Dto\GoalProgressDto;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class GoalCollection extends ResourceCollection implements PaginatedResourceCollection
{
/**
* @var array<string, GoalProgressDto>
*/
private array $progressByGoalId;
/**
* @param array<string, GoalProgressDto> $progressByGoalId
*/
public function __construct($resource, array $progressByGoalId)
{
parent::__construct($resource);
$this->progressByGoalId = $progressByGoalId;
}
protected function collects(): ?string
{
return null;
}
/**
* Transform the resource collection into an array.
*
* @return array<array<string, string|bool|int|null|array<string, string|bool|int|null|array<int, string>>>>
*/
public function toArray(Request $request): array
{
return $this->collection->map(function (Goal $goal) use ($request): array {
return (new GoalResource($goal, $this->progressByGoalId[$goal->getKey()]))
->toArray($request);
})->all();
}
}

View File

@@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Goal;
use App\Http\Resources\V1\BaseResource;
use App\Models\Goal;
use App\Service\Dto\GoalProgressDto;
use Illuminate\Http\Request;
/**
* @property Goal $resource
*/
class GoalResource extends BaseResource
{
private GoalProgressDto $progress;
public function __construct(Goal $resource, GoalProgressDto $progress)
{
parent::__construct($resource);
$this->progress = $progress;
}
/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null|array<string, string|bool|int|null|array<int, string>>>
*/
public function toArray(Request $request): array
{
return [
/** @var string $id ID of the goal */
'id' => $this->resource->id,
/** @var string $name Name */
'name' => $this->resource->name,
/** @var string $type personal: a goal a member set for themselves, organization: a goal of the organization (team goals extension) */
'type' => $this->resource->type->value,
/** @var string $comparison Whether the goal is reached with "at least" or "less than" the target time (at_least, less_than) */
'comparison' => $this->resource->comparison->value,
/** @var int $target_seconds Target time in seconds */
'target_seconds' => $this->resource->target_seconds,
/** @var string $period Recurring time frame (day, week, month) */
'period' => $this->resource->period->value,
/** @var string|null $member_id ID of the member whose time entries count towards the goal, null if every member counts */
'member_id' => $this->resource->member_id,
/** @var string|null $member_name Name of the member whose time entries count towards the goal, null if every member counts */
'member_name' => $this->resource->member?->user->name,
/** @var string $timezone Timezone that defines the periods of the goal */
'timezone' => $this->resource->timezone,
/** @var string $week_start Week start that defines weekly periods */
'week_start' => $this->resource->week_start->value,
/** @var bool $is_archived Whether the goal is archived */
'is_archived' => $this->resource->is_archived,
'filters' => [
/** @var array<string>|null $member_ids Filter by member IDs, member IDs are OR combined, only for goals that count every member */
'member_ids' => $this->resource->filters->memberIds?->toArray(),
/** @var array<string>|null $project_ids Filter by project IDs, project IDs are OR combined */
'project_ids' => $this->resource->filters->projectIds?->toArray(),
/** @var array<string>|null $task_ids Filter by task IDs, task IDs are OR combined */
'task_ids' => $this->resource->filters->taskIds?->toArray(),
/** @var array<string>|null $tag_ids Filter by tag IDs, tag IDs are OR combined */
'tag_ids' => $this->resource->filters->tagIds?->toArray(),
/** @var string|null $tag_match_type Tag match type (contains, not_contains) */
'tag_match_type' => $this->resource->filters->tagMatchType?->value,
/** @var array<string>|null $client_ids Filter by client IDs, client IDs are OR combined */
'client_ids' => $this->resource->filters->clientIds?->toArray(),
/** @var bool|null $billable Filter by billable status */
'billable' => $this->resource->filters->billable,
/** @var string|null $time_entry_type Filter by time entry type (work, break) */
'time_entry_type' => $this->resource->filters->timeEntryType?->value,
],
'progress' => [
/** @var string $period_start Start of the current period (inclusive) */
'period_start' => $this->formatDateTime($this->progress->periodStart),
/** @var string $period_end End of the current period (exclusive) */
'period_end' => $this->formatDateTime($this->progress->periodEnd),
/** @var int $tracked_seconds Seconds tracked in the current period that match the filters, incl. the running time entry */
'tracked_seconds' => $this->progress->trackedSeconds,
/** @var string $status Status in the current period (in_progress, achieved, on_track, exceeded) */
'status' => $this->progress->status->value,
],
/** @var string $created_at Date when the goal was created */
'created_at' => $this->formatDateTime($this->resource->created_at),
/** @var string $updated_at Date when the goal was last updated */
'updated_at' => $this->formatDateTime($this->resource->updated_at),
];
}
}

View File

@@ -1,114 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\GoalComparison;
use App\Enums\GoalPeriod;
use App\Enums\GoalType;
use App\Enums\Weekday;
use App\Models\Concerns\HasUuids;
use App\Service\Dto\GoalFiltersDto;
use Database\Factories\GoalFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* @property string $id
* @property string $name
* @property GoalType $type
* @property GoalComparison $comparison
* @property int $target_seconds
* @property GoalPeriod $period
* @property GoalFiltersDto $filters
* @property string $timezone
* @property Weekday $week_start
* @property Carbon|null $archived_at
* @property-read bool $is_archived
* @property string $organization_id
* @property string|null $member_id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property-read Organization $organization
* @property-read Member|null $member
*
* @method static GoalFactory factory()
*/
class Goal extends Model
{
/** @use HasFactory<GoalFactory> */
use HasFactory;
use HasUuids;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'type' => GoalType::class,
'comparison' => GoalComparison::class,
'target_seconds' => 'int',
'period' => GoalPeriod::class,
'filters' => GoalFiltersDto::class,
'week_start' => Weekday::class,
'archived_at' => 'datetime',
];
/**
* @return BelongsTo<Organization, $this>
*/
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
/**
* The member whose time entries count towards the goal, null for goals that count every member.
*
* @return BelongsTo<Member, $this>
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class, 'member_id');
}
public function isPersonalGoalOf(Member $member): bool
{
return $this->type === GoalType::Personal && $this->member_id === $member->getKey();
}
/**
* @return Attribute<bool, never>
*/
protected function isArchived(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => isset($attributes['archived_at']),
);
}
/**
* @param Builder<Goal> $builder
* @return Builder<Goal>
*/
public function scopeArchived(Builder $builder): Builder
{
return $builder->whereNotNull('archived_at');
}
/**
* @param Builder<Goal> $builder
* @return Builder<Goal>
*/
public function scopeNotArchived(Builder $builder): Builder
{
return $builder->whereNull('archived_at');
}
}

View File

@@ -28,7 +28,6 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
* @property-read User $user
* @property-read Collection<int, ProjectMember> $projectMembers
* @property-read Collection<int, TimeEntry> $timeEntries
* @property-read Collection<int, Goal> $goals
*
* @method static MemberFactory factory()
*/
@@ -79,14 +78,4 @@ class Member extends Pivot implements AuditableContract
{
return $this->hasMany(ProjectMember::class, 'member_id');
}
/**
* Goals whose progress counts the time entries of this member.
*
* @return HasMany<Goal, $this>
*/
public function goals(): HasMany
{
return $this->hasMany(Goal::class, 'member_id');
}
}

View File

@@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
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.
*
* @return BelongsToMany<User, $this, Member, 'membership'>
* @return BelongsToMany<User, $this, Pivot, 'membership'>
*/
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
{

View File

@@ -20,6 +20,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
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
{
@@ -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
{

View File

@@ -17,7 +17,6 @@ use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use App\Service\BillingContract;
use App\Service\GoalsContract;
use App\Service\IpLookup\IpLookupServiceContract;
use App\Service\IpLookup\NoIpLookupService;
use App\Service\PermissionStore;
@@ -26,7 +25,7 @@ use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Section;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -104,7 +103,6 @@ class AppServiceProvider extends ServiceProvider
// Extensions
$this->app->bind(IpLookupServiceContract::class, NoIpLookupService::class);
$this->app->bind(BillingContract::class);
$this->app->bind(GoalsContract::class);
// Storage
// The local driver ignores the ResponseContentDisposition option of temporaryUrl,

View File

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

View File

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

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