mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-08 05:43:18 +01:00
Compare commits
8 Commits
v0.21.0
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1abe73241 | ||
|
|
0ef944a896 | ||
|
|
2072223f19 | ||
|
|
9ea774c1ac | ||
|
|
d1f7e206f2 | ||
|
|
5f23ce98d7 | ||
|
|
400df23035 | ||
|
|
981bad5414 |
4
.env.ci
4
.env.ci
@@ -6,6 +6,7 @@ APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_FORCE_HTTPS=false
|
||||
APP_ENABLE_REGISTRATION=true
|
||||
SUPER_ADMINS=admin@example.com
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
@@ -64,3 +65,6 @@ GOTENBERG_URL=http://localhost:3000
|
||||
|
||||
# Octane
|
||||
OCTANE_SERVER=frankenphp
|
||||
|
||||
# Inertia
|
||||
INERTIA_SSR_ENABLED=false
|
||||
|
||||
@@ -88,3 +88,6 @@ 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
|
||||
|
||||
@@ -13,3 +13,5 @@ LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=pgsql
|
||||
|
||||
INERTIA_SSR_ENABLED=false
|
||||
|
||||
31
.github/workflows/extension-manifest-check.yml
vendored
Normal file
31
.github/workflows/extension-manifest-check.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Extension Manifest Check
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- extensions/manifest.json
|
||||
- .github/workflows/extension-manifest-check.yml
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
check-refs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Extension refs must be pinned to a release tag (vX.Y.Z) or a full commit
|
||||
# SHA. Branch names (main, feature/foo, ...) are only acceptable while
|
||||
# developing and must be replaced before the pull request is merged.
|
||||
- name: "Check extension refs are pinned"
|
||||
run: |
|
||||
failed=0
|
||||
while IFS=$'\t' read -r name ref; do
|
||||
if [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.]+)?$ ]] || [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "✅ $name: $ref"
|
||||
else
|
||||
echo "::error file=extensions/manifest.json::Extension \"$name\" uses ref \"$ref\", which is not a release tag or commit SHA. Pin it before merging."
|
||||
failed=1
|
||||
fi
|
||||
done < <(jq -r 'to_entries[] | [.key, .value.ref] | @tsv' extensions/manifest.json)
|
||||
exit $failed
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,6 +6,7 @@ dist
|
||||
/public/storage
|
||||
/public/css
|
||||
/public/js
|
||||
/public/fonts/filament
|
||||
/public/vendor
|
||||
/lang/vendor
|
||||
/storage/*.key
|
||||
@@ -43,4 +44,5 @@ yarn-error.log
|
||||
/data
|
||||
/config/caddy
|
||||
/config/composer
|
||||
/config/psysh
|
||||
/AGENTS.md
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.
|
||||
@@ -26,7 +27,7 @@ class ActiveUserProvider extends EloquentUserProvider
|
||||
* @param Model|null $model
|
||||
* @return Builder<Model>
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
protected function newModelQuery($model = null): Builder
|
||||
{
|
||||
$query = parent::newModelQuery($model);
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
{
|
||||
@@ -101,7 +102,7 @@ class SelfHostDatabaseConsistency extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, \stdClass> $problems
|
||||
* @param Collection<int, stdClass> $problems
|
||||
*/
|
||||
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Console\Commands\SelfHost;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Str;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
use phpseclib4\Crypt\RSA;
|
||||
|
||||
class SelfHostGenerateKeysCommand extends Command
|
||||
{
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Api;
|
||||
|
||||
use Override;
|
||||
|
||||
class EntityStillInUseApiException extends ApiException
|
||||
{
|
||||
private string $modelToDelete;
|
||||
@@ -32,7 +34,7 @@ class EntityStillInUseApiException extends ApiException
|
||||
/**
|
||||
* Get the translated message for the exception.
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getTranslatedMessage(): string
|
||||
{
|
||||
return __('exceptions.api.'.$this->getKey(), [
|
||||
|
||||
@@ -4,13 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\AuditResource\Pages;
|
||||
use App\Filament\Resources\AuditResource\Pages\CreateAudit;
|
||||
use App\Filament\Resources\AuditResource\Pages\ListAudits;
|
||||
use App\Filament\Resources\AuditResource\Pages\ViewAudit;
|
||||
use App\Models\Audit;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Str;
|
||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||
@@ -19,32 +23,32 @@ class AuditResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Audit::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-archive-box';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-archive-box';
|
||||
|
||||
protected static ?string $navigationGroup = 'System';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('user_type')
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('user_type')
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('user_id'),
|
||||
Forms\Components\TextInput::make('event')
|
||||
TextInput::make('user_id'),
|
||||
TextInput::make('event')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('auditable_type')
|
||||
TextInput::make('auditable_type')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('auditable_id')
|
||||
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')
|
||||
Textarea::make('url'),
|
||||
TextInput::make('ip_address'),
|
||||
TextInput::make('user_agent')
|
||||
->maxLength(1023),
|
||||
Forms\Components\TextInput::make('tags')
|
||||
TextInput::make('tags')
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
@@ -53,27 +57,27 @@ class AuditResource extends Resource
|
||||
{
|
||||
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'),
|
||||
TextColumn::make('user.name'),
|
||||
TextColumn::make('event'),
|
||||
TextColumn::make('auditable_type'),
|
||||
TextColumn::make('auditable_id'),
|
||||
IconColumn::make('was_command')
|
||||
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
|
||||
->boolean(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->sortable()
|
||||
->dateTime(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->sortable()
|
||||
->dateTime(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
@@ -87,9 +91,9 @@ class AuditResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListAudits::route('/'),
|
||||
'create' => Pages\CreateAudit::route('/create'),
|
||||
'view' => Pages\ViewAudit::route('/{record}'),
|
||||
'index' => ListAudits::route('/'),
|
||||
'create' => CreateAudit::route('/create'),
|
||||
'view' => ViewAudit::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ClientResource\Pages;
|
||||
use App\Filament\Resources\ClientResource\Pages\CreateClient;
|
||||
use App\Filament\Resources\ClientResource\Pages\EditClient;
|
||||
use App\Filament\Resources\ClientResource\Pages\ListClients;
|
||||
use App\Models\Client;
|
||||
use 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\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -18,16 +23,16 @@ class ClientResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Client::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-briefcase';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-briefcase';
|
||||
|
||||
protected static ?string $navigationGroup = 'Timetracking';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
||||
|
||||
protected static ?int $navigationSort = 4;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required(),
|
||||
@@ -43,17 +48,17 @@ class ClientResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->label('Name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('organization.name')
|
||||
TextColumn::make('organization.name')
|
||||
->sortable()
|
||||
->label('Organization'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->label('Created at')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->label('Updated at')
|
||||
->sortable(),
|
||||
])
|
||||
@@ -68,12 +73,12 @@ class ClientResource extends Resource
|
||||
->relationship('organization', 'id')
|
||||
->searchable(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -88,9 +93,9 @@ class ClientResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListClients::route('/'),
|
||||
'create' => Pages\CreateClient::route('/create'),
|
||||
'edit' => Pages\EditClient::route('/{record}/edit'),
|
||||
'index' => ListClients::route('/'),
|
||||
'create' => CreateClient::route('/create'),
|
||||
'edit' => EditClient::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ClientResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ClientResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditClient extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditClient extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ClientResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ClientResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListClients extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListClients extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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\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\Schemas\Schema;
|
||||
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 $navigationIcon = 'heroicon-o-exclamation-circle';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-circle';
|
||||
|
||||
protected static ?string $navigationGroup = 'System';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return (string) FailedJob::query()->count();
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
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([])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
BulkAction::make('retry')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->label('Retry selected')
|
||||
@@ -91,7 +91,7 @@ class FailedJobResource extends Resource
|
||||
}),
|
||||
DeleteBulkAction::make(),
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
DeleteAction::make(),
|
||||
ViewAction::make(),
|
||||
Action::make('retry')
|
||||
|
||||
@@ -5,14 +5,21 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Filament\Resources\OrganizationInvitationResource\Pages;
|
||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\EditOrganizationInvitation;
|
||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\ListOrganizationInvitations;
|
||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\ViewOrganizationInvitation;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Service\OrganizationInvitationService;
|
||||
use Filament\Forms;
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -22,18 +29,18 @@ class OrganizationInvitationResource extends Resource
|
||||
|
||||
protected static ?string $label = 'Invitations';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-user-plus';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user-plus';
|
||||
|
||||
protected static ?string $navigationGroup = 'Users';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
||||
|
||||
protected static ?int $navigationSort = 9;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
return $schema
|
||||
->columns(1)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('email')
|
||||
->components([
|
||||
TextInput::make('email')
|
||||
->label('Email')
|
||||
->disabledOn(['edit'])
|
||||
->required(),
|
||||
@@ -45,11 +52,11 @@ class OrganizationInvitationResource extends Resource
|
||||
->searchable(['name'])
|
||||
->disabledOn(['edit'])
|
||||
->required(),
|
||||
Forms\Components\DateTimePicker::make('created_at')
|
||||
DateTimePicker::make('created_at')
|
||||
->label('Created At')
|
||||
->hiddenOn(['create'])
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('updated_at')
|
||||
DateTimePicker::make('updated_at')
|
||||
->label('Updated At')
|
||||
->hiddenOn(['create'])
|
||||
->disabled(),
|
||||
@@ -60,17 +67,17 @@ class OrganizationInvitationResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('organization.name')
|
||||
TextColumn::make('organization.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
TextColumn::make('email')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('role'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('role'),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->label('Updated At')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
@@ -80,13 +87,13 @@ class OrganizationInvitationResource extends Resource
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\BulkAction::make('resend')
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('resend')
|
||||
->label('Resend')
|
||||
->action(function (Collection $records): void {
|
||||
foreach ($records as $organizationInvite) {
|
||||
@@ -106,9 +113,9 @@ class OrganizationInvitationResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListOrganizationInvitations::route('/'),
|
||||
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'),
|
||||
'view' => Pages\ViewOrganizationInvitation::route('/{record}'),
|
||||
'index' => ListOrganizationInvitations::route('/'),
|
||||
'edit' => EditOrganizationInvitation::route('/{record}/edit'),
|
||||
'view' => ViewOrganizationInvitation::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\OrganizationInvitationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OrganizationInvitationResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditOrganizationInvitation extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditOrganizationInvitation extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Filament\Resources\OrganizationResource\Pages;
|
||||
use App\Filament\Resources\OrganizationResource\Pages\CreateOrganization;
|
||||
use App\Filament\Resources\OrganizationResource\Pages\EditOrganization;
|
||||
use App\Filament\Resources\OrganizationResource\Pages\ListOrganizations;
|
||||
use App\Filament\Resources\OrganizationResource\Pages\ViewOrganization;
|
||||
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
|
||||
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
|
||||
use App\Models\Organization;
|
||||
@@ -20,38 +23,45 @@ 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 Filament\Forms;
|
||||
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 Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
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 $navigationIcon = 'heroicon-o-building-office-2';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
|
||||
|
||||
protected static ?string $navigationGroup = 'Users';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
||||
|
||||
protected static ?int $navigationSort = 7;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
return $schema
|
||||
->columns(1)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Toggle::make('personal_team')
|
||||
Toggle::make('personal_team')
|
||||
->label('Is personal?')
|
||||
->hiddenOn(['create'])
|
||||
->required(),
|
||||
@@ -79,7 +89,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().')';
|
||||
@@ -89,7 +99,7 @@ class OrganizationResource extends Resource
|
||||
})
|
||||
->required()
|
||||
->searchable(),
|
||||
Forms\Components\TextInput::make('billable_rate')
|
||||
TextInput::make('billable_rate')
|
||||
->label('Billable rate (in Cents)')
|
||||
->nullable()
|
||||
->rules([
|
||||
@@ -99,11 +109,11 @@ class OrganizationResource extends Resource
|
||||
'max:2147483647',
|
||||
])
|
||||
->numeric(),
|
||||
Forms\Components\DateTimePicker::make('created_at')
|
||||
DateTimePicker::make('created_at')
|
||||
->label('Created At')
|
||||
->hiddenOn(['create'])
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('updated_at')
|
||||
DateTimePicker::make('updated_at')
|
||||
->label('Updated At')
|
||||
->hiddenOn(['create'])
|
||||
->disabled(),
|
||||
@@ -117,7 +127,7 @@ class OrganizationResource extends Resource
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('personal_team')
|
||||
IconColumn::make('personal_team')
|
||||
->boolean()
|
||||
->label('Is personal?')
|
||||
->sortable(),
|
||||
@@ -138,9 +148,9 @@ class OrganizationResource extends Resource
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->using(function (Organization $record): void {
|
||||
app(DeletionService::class)->deleteOrganization($record);
|
||||
}),
|
||||
@@ -158,7 +168,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')
|
||||
@@ -172,9 +182,13 @@ class OrganizationResource extends Resource
|
||||
->icon('heroicon-o-inbox-arrow-down')
|
||||
->action(function (Organization $record, array $data): void {
|
||||
try {
|
||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||
try {
|
||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||
} catch (UnableToReadFile) {
|
||||
$file = null;
|
||||
}
|
||||
if ($file === null) {
|
||||
throw new \Exception('File not found');
|
||||
throw new ImportException('File not found');
|
||||
}
|
||||
/** @var string $timezone */
|
||||
$timezone = $data['timezone'];
|
||||
@@ -209,8 +223,8 @@ class OrganizationResource extends Resource
|
||||
}
|
||||
})
|
||||
->tooltip(fn (Organization $record): string => 'Import into '.$record->name)
|
||||
->form([
|
||||
Forms\Components\FileUpload::make('file')
|
||||
->schema([
|
||||
FileUpload::make('file')
|
||||
->label('File')
|
||||
->required(),
|
||||
Select::make('type')
|
||||
@@ -230,7 +244,7 @@ class OrganizationResource extends Resource
|
||||
->required(),
|
||||
]),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -245,10 +259,10 @@ class OrganizationResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListOrganizations::route('/'),
|
||||
'create' => Pages\CreateOrganization::route('/create'),
|
||||
'edit' => Pages\EditOrganization::route('/{record}/edit'),
|
||||
'view' => Pages\ViewOrganization::route('/{record}'),
|
||||
'index' => ListOrganizations::route('/'),
|
||||
'create' => CreateOrganization::route('/create'),
|
||||
'edit' => EditOrganization::route('/{record}/edit'),
|
||||
'view' => ViewOrganization::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -14,7 +15,7 @@ class EditOrganization extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
OrganizationResource\Actions\DeleteOrganization::make(),
|
||||
DeleteOrganization::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\OrganizationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OrganizationResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListOrganizations extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListOrganizations extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,12 +9,17 @@ 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\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -25,10 +30,10 @@ class InvitationsRelationManager extends RelationManager
|
||||
|
||||
protected static ?string $title = 'Invitations';
|
||||
|
||||
public function form(Form $form): Form
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('email')
|
||||
->label('Email')
|
||||
->disabledOn(['edit'])
|
||||
@@ -53,33 +58,33 @@ class InvitationsRelationManager extends RelationManager
|
||||
->modelLabel('Invitation')
|
||||
->pluralModelLabel('Invitations')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('email'),
|
||||
Tables\Columns\TextColumn::make('role'),
|
||||
TextColumn::make('email'),
|
||||
TextColumn::make('role'),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()
|
||||
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'], Role::from($data['role']), auth()->user());
|
||||
->inviteUser($ownerRecord, $data['email'], ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), auth()->user());
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
Action::make('view')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
->url(fn (OrganizationInvitation $record): string => OrganizationInvitationResource::getUrl('view', [
|
||||
'record' => $record->getKey(),
|
||||
])),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DetachBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -12,14 +12,15 @@ 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\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\AttachAction;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -28,10 +29,10 @@ class UsersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'users';
|
||||
|
||||
public function form(Form $form): Form
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('role')
|
||||
->options(Role::class),
|
||||
TextInput::make('billable_rate')
|
||||
@@ -75,17 +76,17 @@ class UsersRelationManager extends RelationManager
|
||||
->using(function (User $record, array $data): void {
|
||||
/** @var Organization $organization */
|
||||
$organization = $this->getOwnerRecord();
|
||||
app(MemberService::class)->addMember($record, $organization, Role::from($data['role']), true);
|
||||
app(MemberService::class)->addMember($record, $organization, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
Action::make('view')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
->url(fn (User $record): string => UserResource::getUrl('view', [
|
||||
'record' => $record->getKey(),
|
||||
])),
|
||||
Tables\Actions\EditAction::make()
|
||||
EditAction::make()
|
||||
->using(function (User $record, array $data): User {
|
||||
/** @var Organization $organization */
|
||||
$organization = $this->getOwnerRecord();
|
||||
@@ -97,9 +98,10 @@ class UsersRelationManager extends RelationManager
|
||||
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
||||
}
|
||||
|
||||
if ($data['role'] !== $member->role) {
|
||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
||||
if ($newRole->value !== $member->role) {
|
||||
try {
|
||||
app(MemberService::class)->changeRole($member, $organization, Role::from($data['role']), true);
|
||||
app(MemberService::class)->changeRole($member, $organization, $newRole, true);
|
||||
} catch (ApiException $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
@@ -113,7 +115,7 @@ class UsersRelationManager extends RelationManager
|
||||
|
||||
return $record;
|
||||
}),
|
||||
Tables\Actions\DetachAction::make()
|
||||
DetachAction::make()
|
||||
->using(function (User $record): void {
|
||||
/** @var Organization $organization */
|
||||
$organization = $this->getOwnerRecord();
|
||||
@@ -133,7 +135,7 @@ class UsersRelationManager extends RelationManager
|
||||
}
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ProjectMemberResource\Pages;
|
||||
use App\Filament\Resources\ProjectMemberResource\Pages\CreateProjectMember;
|
||||
use App\Filament\Resources\ProjectMemberResource\Pages\EditProjectMember;
|
||||
use App\Filament\Resources\ProjectMemberResource\Pages\ListProjectMembers;
|
||||
use App\Filament\Resources\ProjectMemberResource\Pages\ViewProjectMembers;
|
||||
use App\Models\ProjectMember;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProjectMemberResource extends Resource
|
||||
@@ -18,11 +25,11 @@ class ProjectMemberResource extends Resource
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('billable_rate')
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('billable_rate')
|
||||
->label('Billable rate (in Cents)')
|
||||
->nullable()
|
||||
->rules([
|
||||
@@ -32,10 +39,10 @@ class ProjectMemberResource extends Resource
|
||||
'max:2147483647',
|
||||
])
|
||||
->numeric(),
|
||||
Forms\Components\Select::make('user_id')
|
||||
Select::make('user_id')
|
||||
->relationship('user', 'name')
|
||||
->required(),
|
||||
Forms\Components\Select::make('member_id')
|
||||
Select::make('member_id')
|
||||
->relationship('member', 'id')
|
||||
->required(),
|
||||
]);
|
||||
@@ -45,17 +52,17 @@ class ProjectMemberResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
TextColumn::make('id')
|
||||
->label('ID'),
|
||||
Tables\Columns\TextColumn::make('billable_rate')
|
||||
TextColumn::make('billable_rate')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('project.name'),
|
||||
Tables\Columns\TextColumn::make('user.name'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('project.name'),
|
||||
TextColumn::make('user.name'),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
@@ -63,12 +70,12 @@ class ProjectMemberResource extends Resource
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -83,10 +90,10 @@ class ProjectMemberResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListProjectMembers::route('/'),
|
||||
'create' => Pages\CreateProjectMember::route('/create'),
|
||||
'edit' => Pages\EditProjectMember::route('/{record}/edit'),
|
||||
'view' => Pages\ViewProjectMembers::route('/{record}'),
|
||||
'index' => ListProjectMembers::route('/'),
|
||||
'create' => CreateProjectMember::route('/create'),
|
||||
'edit' => EditProjectMember::route('/{record}/edit'),
|
||||
'view' => ViewProjectMembers::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ProjectMemberResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ProjectMemberResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditProjectMember extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditProjectMember extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ProjectMemberResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ProjectMemberResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProjectMembers extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListProjectMembers extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,14 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ProjectResource\Pages;
|
||||
use App\Filament\Resources\ProjectResource\Pages\CreateProject;
|
||||
use App\Filament\Resources\ProjectResource\Pages\EditProject;
|
||||
use App\Filament\Resources\ProjectResource\Pages\ListProjects;
|
||||
use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager;
|
||||
use App\Models\Project;
|
||||
use Filament\Forms;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\ColorColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
@@ -21,24 +26,24 @@ class ProjectResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Project::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-folder';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-folder';
|
||||
|
||||
protected static ?string $navigationGroup = 'Timetracking';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
ColorPicker::make('color')
|
||||
->label('Color')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('billable_rate')
|
||||
TextInput::make('billable_rate')
|
||||
->label('Billable rate (in Cents)')
|
||||
->nullable()
|
||||
->rules([
|
||||
@@ -48,7 +53,7 @@ class ProjectResource extends Resource
|
||||
'max:2147483647',
|
||||
])
|
||||
->numeric(),
|
||||
Forms\Components\Select::make('organization_id')
|
||||
Select::make('organization_id')
|
||||
->relationship(name: 'organization', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
->required(),
|
||||
@@ -81,12 +86,12 @@ class ProjectResource extends Resource
|
||||
->searchable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -101,9 +106,9 @@ class ProjectResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListProjects::route('/'),
|
||||
'create' => Pages\CreateProject::route('/create'),
|
||||
'edit' => Pages\EditProject::route('/{record}/edit'),
|
||||
'index' => ListProjects::route('/'),
|
||||
'create' => CreateProject::route('/create'),
|
||||
'edit' => EditProject::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ProjectResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ProjectResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditProject extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditProject extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ProjectResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ProjectResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProjects extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListProjects extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace App\Filament\Resources\ProjectResource\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\ProjectMemberResource;
|
||||
use App\Models\ProjectMember;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProjectMembersRelationManager extends RelationManager
|
||||
@@ -18,10 +18,10 @@ class ProjectMembersRelationManager extends RelationManager
|
||||
|
||||
protected static string $relationship = 'members';
|
||||
|
||||
public function form(Form $form): Form
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ class ProjectMembersRelationManager extends RelationManager
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('user.name'),
|
||||
Tables\Columns\TextColumn::make('billable_rate')
|
||||
TextColumn::make('user.name'),
|
||||
TextColumn::make('billable_rate')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
])
|
||||
@@ -40,7 +40,7 @@ class ProjectMembersRelationManager extends RelationManager
|
||||
])
|
||||
->headerActions([
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
Action::make('view')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
@@ -54,7 +54,7 @@ class ProjectMembersRelationManager extends RelationManager
|
||||
]))
|
||||
->openUrlInNewTab(),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ReportResource\Pages;
|
||||
use App\Filament\Resources\ReportResource\Pages\EditReport;
|
||||
use App\Filament\Resources\ReportResource\Pages\ListReports;
|
||||
use App\Filament\Resources\ReportResource\Pages\ViewReport;
|
||||
use App\Models\Report;
|
||||
use App\Service\Dto\ReportPropertiesDto;
|
||||
use Filament\Forms;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
@@ -24,22 +29,22 @@ class ReportResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Report::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-chart-bar';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-chart-bar';
|
||||
|
||||
protected static ?string $navigationGroup = 'Timetracking';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
||||
|
||||
protected static ?int $navigationSort = 7;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
return $schema
|
||||
->columns(1)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('description')
|
||||
TextInput::make('description')
|
||||
->label('Description')
|
||||
->nullable()
|
||||
->maxLength(255),
|
||||
@@ -49,13 +54,13 @@ class ReportResource extends Resource
|
||||
DateTimePicker::make('public_until')
|
||||
->label('Public until')
|
||||
->nullable(),
|
||||
Forms\Components\Select::make('organization_id')
|
||||
Select::make('organization_id')
|
||||
->label('Organization')
|
||||
->relationship(name: 'organization', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
->disabled()
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('share_secret')
|
||||
TextInput::make('share_secret')
|
||||
->label('Share Secret')
|
||||
->nullable(),
|
||||
PrettyJsonField::make('properties')
|
||||
@@ -109,18 +114,18 @@ class ReportResource extends Resource
|
||||
->relationship('organization', 'id')
|
||||
->searchable(),
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
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),
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -133,9 +138,9 @@ class ReportResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListReports::route('/'),
|
||||
'edit' => Pages\EditReport::route('/{record}/edit'),
|
||||
'view' => Pages\ViewReport::route('/{record}'),
|
||||
'index' => ListReports::route('/'),
|
||||
'edit' => EditReport::route('/{record}/edit'),
|
||||
'view' => ViewReport::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\ReportResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ReportResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditReport extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditReport extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TagResource\Pages;
|
||||
use App\Filament\Resources\TagResource\Pages\CreateTag;
|
||||
use App\Filament\Resources\TagResource\Pages\EditTag;
|
||||
use App\Filament\Resources\TagResource\Pages\ListTags;
|
||||
use App\Models\Tag;
|
||||
use Filament\Forms;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -18,20 +23,20 @@ class TagResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Tag::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-tag';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-tag';
|
||||
|
||||
protected static ?string $navigationGroup = 'Timetracking';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required(),
|
||||
Forms\Components\Select::make('organization_id')
|
||||
Select::make('organization_id')
|
||||
->relationship(name: 'organization', titleAttribute: 'name')
|
||||
->label('Organization')
|
||||
->searchable(['name'])
|
||||
@@ -43,17 +48,17 @@ class TagResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->label('Name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('organization.name')
|
||||
TextColumn::make('organization.name')
|
||||
->sortable()
|
||||
->label('Organization'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->label('Created at')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->label('Updated at')
|
||||
->sortable(),
|
||||
])
|
||||
@@ -68,12 +73,12 @@ class TagResource extends Resource
|
||||
->relationship('organization', 'id')
|
||||
->searchable(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -88,9 +93,9 @@ class TagResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTags::route('/'),
|
||||
'create' => Pages\CreateTag::route('/create'),
|
||||
'edit' => Pages\EditTag::route('/{record}/edit'),
|
||||
'index' => ListTags::route('/'),
|
||||
'create' => CreateTag::route('/create'),
|
||||
'edit' => EditTag::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\TagResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TagResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTag extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditTag extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\TagResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TagResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTags extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListTags extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TaskResource\Pages;
|
||||
use App\Filament\Resources\TaskResource\Pages\CreateTask;
|
||||
use App\Filament\Resources\TaskResource\Pages\EditTask;
|
||||
use App\Filament\Resources\TaskResource\Pages\ListTasks;
|
||||
use App\Models\Task;
|
||||
use Filament\Forms;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -18,17 +23,17 @@ class TaskResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Task::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-list-bullet';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-list-bullet';
|
||||
|
||||
protected static ?string $navigationGroup = 'Timetracking';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
@@ -47,16 +52,16 @@ class TaskResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('project.name')
|
||||
TextColumn::make('project.name')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('organization.name')
|
||||
TextColumn::make('organization.name')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
@@ -70,12 +75,12 @@ class TaskResource extends Resource
|
||||
->searchable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -90,9 +95,9 @@ class TaskResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTasks::route('/'),
|
||||
'create' => Pages\CreateTask::route('/create'),
|
||||
'edit' => Pages\EditTask::route('/{record}/edit'),
|
||||
'index' => ListTasks::route('/'),
|
||||
'create' => CreateTask::route('/create'),
|
||||
'edit' => EditTask::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\TaskResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TaskResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTask extends EditRecord
|
||||
@@ -15,7 +15,7 @@ class EditTask extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\TaskResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TaskResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTasks extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListTasks extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,16 +4,20 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TimeEntryResource\Pages;
|
||||
use App\Filament\Resources\TimeEntryResource\Pages\CreateTimeEntry;
|
||||
use App\Filament\Resources\TimeEntryResource\Pages\EditTimeEntry;
|
||||
use App\Filament\Resources\TimeEntryResource\Pages\ListTimeEntries;
|
||||
use App\Models\Member;
|
||||
use App\Models\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\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
@@ -23,16 +27,16 @@ class TimeEntryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = TimeEntry::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-clock';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clock';
|
||||
|
||||
protected static ?string $navigationGroup = 'Timetracking';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('id')
|
||||
->label('ID')
|
||||
->readOnly()
|
||||
@@ -111,12 +115,12 @@ class TimeEntryResource extends Resource
|
||||
->searchable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -131,9 +135,9 @@ class TimeEntryResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTimeEntries::route('/'),
|
||||
'create' => Pages\CreateTimeEntry::route('/create'),
|
||||
'edit' => Pages\EditTimeEntry::route('/{record}/edit'),
|
||||
'index' => ListTimeEntries::route('/'),
|
||||
'create' => CreateTimeEntry::route('/create'),
|
||||
'edit' => EditTimeEntry::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Filament\Resources\TimeEntryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TimeEntryResource;
|
||||
use App\Models\Member;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTimeEntry extends EditRecord
|
||||
@@ -16,7 +16,7 @@ class EditTimeEntry extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
DeleteAction::make()
|
||||
->icon('heroicon-m-trash'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\TimeEntryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TimeEntryResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTimeEntries extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListTimeEntries extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,12 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TokenResource\Pages;
|
||||
use App\Filament\Resources\TokenResource\Pages\ListTokens;
|
||||
use App\Filament\Resources\TokenResource\Pages\ViewToken;
|
||||
use App\Models\Passport\Token;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
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\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -18,48 +24,48 @@ class TokenResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Token::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-key';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-key';
|
||||
|
||||
protected static ?string $navigationGroup = 'Auth';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Auth';
|
||||
|
||||
protected static ?int $navigationSort = 6;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
return $schema
|
||||
->columns(1)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('id')
|
||||
->components([
|
||||
TextInput::make('id')
|
||||
->label('ID')
|
||||
->disabled()
|
||||
->visibleOn(['update', 'show'])
|
||||
->readOnly()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('owner_id')
|
||||
Select::make('owner_id')
|
||||
->label('User')
|
||||
->relationship(name: 'user', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
->disabled()
|
||||
->required(),
|
||||
Forms\Components\Select::make('client_id')
|
||||
Select::make('client_id')
|
||||
->label('Client')
|
||||
->relationship(name: 'client', titleAttribute: 'name')
|
||||
->searchable(['name'])
|
||||
->required(),
|
||||
Forms\Components\Toggle::make('revoked')
|
||||
Toggle::make('revoked')
|
||||
->label('Revoked')
|
||||
->required(),
|
||||
Forms\Components\DateTimePicker::make('expires_at')
|
||||
DateTimePicker::make('expires_at')
|
||||
->label('Expires At')
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('created_at')
|
||||
DateTimePicker::make('created_at')
|
||||
->label('Created At')
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('updated_at')
|
||||
DateTimePicker::make('updated_at')
|
||||
->label('Updated At')
|
||||
->disabled(),
|
||||
]);
|
||||
@@ -69,32 +75,32 @@ class TokenResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('user.name')
|
||||
TextColumn::make('user.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('client.name')
|
||||
TextColumn::make('client.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('personal_access_client')
|
||||
IconColumn::make('personal_access_client')
|
||||
->state(function (Token $token): bool {
|
||||
return in_array('personal_access', $token->client->grant_types ?? [], true);
|
||||
})
|
||||
->boolean()
|
||||
->label('API token?'),
|
||||
Tables\Columns\IconColumn::make('revoked')
|
||||
IconColumn::make('revoked')
|
||||
->boolean()
|
||||
->label('Revoked?')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('expires_at')
|
||||
TextColumn::make('expires_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
@@ -120,10 +126,10 @@ class TokenResource extends Resource
|
||||
TernaryFilter::make('revoked')
|
||||
->label('Revoked?'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -136,8 +142,8 @@ class TokenResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTokens::route('/'),
|
||||
'view' => Pages\ViewToken::route('/{record}'),
|
||||
'index' => ListTokens::route('/'),
|
||||
'view' => ViewToken::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,30 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\Weekday;
|
||||
use App\Exceptions\Api\ApiException;
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Filament\Resources\UserResource\Pages\CreateUser;
|
||||
use App\Filament\Resources\UserResource\Pages\EditUser;
|
||||
use App\Filament\Resources\UserResource\Pages\ListUsers;
|
||||
use App\Filament\Resources\UserResource\Pages\ViewUser;
|
||||
use App\Filament\Resources\UserResource\RelationManagers\OrganizationsRelationManager;
|
||||
use App\Filament\Resources\UserResource\RelationManagers\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\Forms;
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -28,26 +37,26 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
|
||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-user';
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user';
|
||||
|
||||
protected static ?string $navigationGroup = 'Users';
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
||||
|
||||
protected static ?int $navigationSort = 6;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
/** @var User|null $record */
|
||||
$record = $form->getRecord();
|
||||
$record = $schema->getRecord();
|
||||
|
||||
return $form
|
||||
return $schema
|
||||
->columns(1)
|
||||
->schema([
|
||||
->components([
|
||||
TextInput::make('id')
|
||||
->label('ID')
|
||||
->disabled()
|
||||
@@ -69,23 +78,23 @@ class UserResource extends Resource
|
||||
'email:rfc,strict',
|
||||
])
|
||||
->maxLength(255),
|
||||
Forms\Components\Toggle::make('is_placeholder')
|
||||
Toggle::make('is_placeholder')
|
||||
->label('Is Placeholder?')
|
||||
->hiddenOn(['create'])
|
||||
->disabledOn(['edit']),
|
||||
Forms\Components\DateTimePicker::make('email_verified_at')
|
||||
DateTimePicker::make('email_verified_at')
|
||||
->label('Email Verified At')
|
||||
->hiddenOn(['create'])
|
||||
->nullable(),
|
||||
Forms\Components\Toggle::make('is_email_verified')
|
||||
Toggle::make('is_email_verified')
|
||||
->label('Email Verified?')
|
||||
->visibleOn(['create']),
|
||||
Forms\Components\Select::make('timezone')
|
||||
Select::make('timezone')
|
||||
->label('Timezone')
|
||||
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
|
||||
->searchable()
|
||||
->required(),
|
||||
Forms\Components\Select::make('week_start')
|
||||
Select::make('week_start')
|
||||
->label('Week Start')
|
||||
->options(Weekday::class)
|
||||
->required(),
|
||||
@@ -103,10 +112,10 @@ class UserResource extends Resource
|
||||
->visibleOn(['create'])
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('currency')
|
||||
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().')';
|
||||
@@ -117,11 +126,11 @@ class UserResource extends Resource
|
||||
->required()
|
||||
->visibleOn(['create'])
|
||||
->searchable(),
|
||||
Forms\Components\DateTimePicker::make('created_at')
|
||||
DateTimePicker::make('created_at')
|
||||
->label('Created At')
|
||||
->hiddenOn(['create'])
|
||||
->disabled(),
|
||||
Forms\Components\DateTimePicker::make('updated_at')
|
||||
DateTimePicker::make('updated_at')
|
||||
->label('Updated At')
|
||||
->hiddenOn(['create'])
|
||||
->disabled(),
|
||||
@@ -132,25 +141,25 @@ class UserResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
TextColumn::make('email')
|
||||
->icon('heroicon-m-envelope')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('is_real_user')
|
||||
IconColumn::make('is_real_user')
|
||||
->getStateUsing(fn (User $record): bool => ! $record->is_placeholder)
|
||||
->label('Real user?')
|
||||
->boolean(),
|
||||
Tables\Columns\IconColumn::make('email_verified')
|
||||
IconColumn::make('email_verified')
|
||||
->getStateUsing(fn (User $record): bool => $record->email_verified_at !== null)
|
||||
->label('Email verified?')
|
||||
->boolean(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
@@ -178,7 +187,7 @@ class UserResource extends Resource
|
||||
->attribute('email_verified_at')
|
||||
->nullable(),
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
Impersonate::make()->before(function (User $record): void {
|
||||
if ($record->currentOrganization === null) {
|
||||
$organization = $record->organizations()->where('personal_team', '=', true)->first();
|
||||
@@ -191,8 +200,8 @@ class UserResource extends Resource
|
||||
app(UserService::class)->switchCurrentOrganization($record, $organization);
|
||||
}
|
||||
}),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->hidden(fn (User $record) => $record->is(Auth::user()))
|
||||
->using(function (User $record): void {
|
||||
try {
|
||||
@@ -207,8 +216,8 @@ class UserResource extends Resource
|
||||
}
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkAction::make('Resend verification email')
|
||||
->toolbarActions([
|
||||
BulkAction::make('Resend verification email')
|
||||
->icon('heroicon-o-paper-airplane')
|
||||
->action(function (Collection $records): void {
|
||||
foreach ($records as $user) {
|
||||
@@ -230,10 +239,10 @@ class UserResource extends Resource
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'create' => Pages\CreateUser::route('/create'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
'view' => Pages\ViewUser::route('/{record}'),
|
||||
'index' => ListUsers::route('/'),
|
||||
'create' => CreateUser::route('/create'),
|
||||
'edit' => EditUser::route('/{record}/edit'),
|
||||
'view' => ViewUser::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class CreateUser extends CreateRecord
|
||||
$data['email'],
|
||||
$data['password_create'],
|
||||
$data['timezone'],
|
||||
Weekday::from($data['week_start']),
|
||||
$data['week_start'] instanceof Weekday ? $data['week_start'] : Weekday::from($data['week_start']),
|
||||
$data['currency'],
|
||||
verifyEmail: (bool) $data['is_email_verified']
|
||||
);
|
||||
|
||||
@@ -5,8 +5,9 @@ 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\Pages\Actions\Impersonate;
|
||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
{
|
||||
@@ -16,7 +17,7 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [
|
||||
Impersonate::make()->record($this->getRecord()),
|
||||
UserResource\Actions\DeleteUser::make(),
|
||||
DeleteUser::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
@@ -15,7 +15,7 @@ class ListUsers extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->icon('heroicon-s-plus'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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\Pages\Actions\Impersonate;
|
||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
||||
|
||||
class ViewUser extends ViewRecord
|
||||
{
|
||||
|
||||
@@ -11,12 +11,13 @@ 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\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -24,10 +25,10 @@ class OrganizationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'organizations';
|
||||
|
||||
public function form(Form $form): Form
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('role')
|
||||
->options(Role::class),
|
||||
]);
|
||||
@@ -46,21 +47,22 @@ class OrganizationsRelationManager extends RelationManager
|
||||
])
|
||||
->headerActions([
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
Action::make('view')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
->url(fn (Organization $record): string => OrganizationResource::getUrl('view', [
|
||||
'record' => $record->getKey(),
|
||||
])),
|
||||
Tables\Actions\EditAction::make()
|
||||
EditAction::make()
|
||||
->using(function (Organization $record, array $data): Organization {
|
||||
/** @var Member $member */
|
||||
$member = $record->getRelation('membership');
|
||||
|
||||
if ($data['role'] !== $member->role) {
|
||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
||||
if ($newRole->value !== $member->role) {
|
||||
try {
|
||||
app(MemberService::class)->changeRole($member, $record, Role::from($data['role']), true);
|
||||
app(MemberService::class)->changeRole($member, $record, $newRole, true);
|
||||
} catch (ApiException $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
@@ -74,7 +76,7 @@ class OrganizationsRelationManager extends RelationManager
|
||||
|
||||
return $record;
|
||||
}),
|
||||
Tables\Actions\DetachAction::make()
|
||||
DetachAction::make()
|
||||
->using(function (Organization $record): void {
|
||||
/** @var User $user */
|
||||
$user = $this->getOwnerRecord();
|
||||
@@ -94,7 +96,7 @@ class OrganizationsRelationManager extends RelationManager
|
||||
}
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace App\Filament\Resources\UserResource\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\OrganizationResource;
|
||||
use App\Models\Organization;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class OwnedOrganizationsRelationManager extends RelationManager
|
||||
@@ -18,10 +18,10 @@ class OwnedOrganizationsRelationManager extends RelationManager
|
||||
|
||||
protected static string $relationship = 'ownedOrganizations';
|
||||
|
||||
public function form(Form $form): Form
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
return $schema
|
||||
->components([
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ class OwnedOrganizationsRelationManager extends RelationManager
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name'),
|
||||
TextColumn::make('name'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
])
|
||||
->actions([
|
||||
->recordActions([
|
||||
Action::make('view')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
@@ -51,7 +51,7 @@ class OwnedOrganizationsRelationManager extends RelationManager
|
||||
]))
|
||||
->openUrlInNewTab(),
|
||||
])
|
||||
->bulkActions([
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ServerOverview extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.widgets.server-overview';
|
||||
protected string $view = 'filament.widgets.server-overview';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
|
||||
@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
|
||||
|
||||
class TimeEntriesCreated extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Time Entries Created';
|
||||
protected ?string $heading = 'Time Entries Created';
|
||||
|
||||
public ?string $filter = 'week';
|
||||
|
||||
@@ -53,7 +53,7 @@ class TimeEntriesCreated extends ChartWidget
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => self::$heading,
|
||||
'label' => $this->heading,
|
||||
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
|
||||
|
||||
class TimeEntriesImported extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Time Entries Imported';
|
||||
protected ?string $heading = 'Time Entries Imported';
|
||||
|
||||
public ?string $filter = 'week';
|
||||
|
||||
@@ -53,7 +53,7 @@ class TimeEntriesImported extends ChartWidget
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => self::$heading,
|
||||
'label' => $this->heading,
|
||||
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -11,7 +11,7 @@ use Flowframe\Trend\TrendValue;
|
||||
|
||||
class UserRegistrations extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'User Registrations';
|
||||
protected ?string $heading = 'User Registrations';
|
||||
|
||||
public ?string $filter = 'week';
|
||||
|
||||
@@ -54,7 +54,7 @@ class UserRegistrations extends ChartWidget
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => self::$heading,
|
||||
'label' => $this->heading,
|
||||
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
class ApiTokenController extends Controller
|
||||
{
|
||||
@@ -62,7 +63,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,6 +33,7 @@ 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
|
||||
{
|
||||
@@ -146,7 +147,7 @@ class MemberController extends Controller
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*
|
||||
* @operationId mergeMember
|
||||
*/
|
||||
|
||||
@@ -55,6 +55,7 @@ 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;
|
||||
|
||||
@@ -190,8 +191,9 @@ class TimeEntryController extends Controller
|
||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||
if ($roundingType !== null && $roundingMinutes !== null) {
|
||||
$select = array_diff($select, ['start', 'end']);
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as 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
|
||||
}
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
@@ -265,7 +267,7 @@ class TimeEntryController extends Controller
|
||||
}
|
||||
$viewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf.blade.php'));
|
||||
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(
|
||||
@@ -293,7 +295,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) {
|
||||
@@ -495,7 +497,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,
|
||||
@@ -512,7 +514,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) {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -16,6 +16,7 @@ use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Features;
|
||||
use stdClass;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
@@ -103,7 +104,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
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -18,7 +19,6 @@ 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,
|
||||
VerifyCsrfToken::class,
|
||||
PreventRequestForgery::class,
|
||||
SubstituteBindings::class,
|
||||
HandleInertiaRequests::class,
|
||||
ShareInertiaData::class,
|
||||
|
||||
22
app/Http/Middleware/AuthenticateFilamentPanel.php
Normal file
22
app/Http/Middleware/AuthenticateFilamentPanel.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Filament\Http\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuthenticateFilamentPanel extends Middleware
|
||||
{
|
||||
/**
|
||||
* The admin panel has no login page of its own (it shares the app's
|
||||
* session-based login), so Filament::getLoginUrl() always returns null.
|
||||
* Fall back to the app's login route so guests are redirected instead
|
||||
* of receiving an empty 401 response.
|
||||
*/
|
||||
protected function redirectTo($request): ?string
|
||||
{
|
||||
return parent::redirectTo($request) ?? ($request->expectsJson() ? null : route('login'));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Models\Organization;
|
||||
use App\Service\BillingContract;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use LogicException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckOrganizationBlocked
|
||||
@@ -25,7 +26,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 */
|
||||
|
||||
19
app/Http/Middleware/PreventRequestForgery.php
Normal file
19
app/Http/Middleware/PreventRequestForgery.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;
|
||||
|
||||
class PreventRequestForgery extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -39,7 +40,7 @@ class TrustHosts extends BaseTrustHosts
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure(Request): Response $next
|
||||
* @param Closure(Request):Response $next
|
||||
*/
|
||||
public function handle(Request $request, $next)
|
||||
{
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -13,11 +13,13 @@ 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
|
||||
@@ -27,7 +29,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
|
||||
{
|
||||
@@ -85,7 +87,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;
|
||||
}
|
||||
@@ -101,7 +103,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;
|
||||
}
|
||||
@@ -117,7 +119,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;
|
||||
}
|
||||
@@ -137,7 +139,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;
|
||||
}
|
||||
@@ -213,7 +215,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;
|
||||
@@ -223,7 +225,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;
|
||||
|
||||
@@ -19,11 +19,13 @@ 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
|
||||
@@ -33,7 +35,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
|
||||
{
|
||||
@@ -97,7 +99,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;
|
||||
}
|
||||
@@ -114,7 +116,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;
|
||||
}
|
||||
@@ -131,7 +133,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;
|
||||
}
|
||||
@@ -152,7 +154,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;
|
||||
}
|
||||
@@ -236,7 +238,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;
|
||||
@@ -246,7 +248,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;
|
||||
|
||||
@@ -17,6 +17,7 @@ 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;
|
||||
@@ -31,7 +32,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
|
||||
{
|
||||
@@ -83,7 +84,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;
|
||||
}
|
||||
@@ -100,7 +101,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;
|
||||
}
|
||||
@@ -117,7 +118,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;
|
||||
}
|
||||
@@ -138,7 +139,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;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ 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
|
||||
@@ -29,7 +31,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
|
||||
{
|
||||
@@ -68,7 +70,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;
|
||||
}
|
||||
@@ -85,7 +87,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;
|
||||
}
|
||||
@@ -102,7 +104,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;
|
||||
}
|
||||
@@ -123,7 +125,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;
|
||||
}
|
||||
@@ -200,7 +202,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;
|
||||
@@ -210,7 +212,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;
|
||||
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -29,7 +30,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
|
||||
{
|
||||
@@ -61,7 +62,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;
|
||||
}
|
||||
@@ -78,7 +79,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;
|
||||
}
|
||||
@@ -95,7 +96,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;
|
||||
}
|
||||
@@ -116,7 +117,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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -26,7 +27,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
|
||||
{
|
||||
@@ -94,7 +95,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.');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
@@ -29,7 +30,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
|
||||
{
|
||||
@@ -105,7 +106,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
|
||||
|
||||
@@ -19,7 +19,6 @@ 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;
|
||||
@@ -100,7 +99,7 @@ class Organization extends Model implements AuditableContract
|
||||
/**
|
||||
* Get all the users that belong to the team.
|
||||
*
|
||||
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||
* @return BelongsToMany<User, $this, Member, 'membership'>
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
@@ -133,7 +132,7 @@ class Organization extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||
* @return BelongsToMany<User, $this, Member, 'membership'>
|
||||
*/
|
||||
public function realUsers(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -20,7 +20,6 @@ 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;
|
||||
@@ -175,7 +174,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||
* @return BelongsToMany<Organization, $this, Member, 'membership'>
|
||||
*/
|
||||
public function organizations(): BelongsToMany
|
||||
{
|
||||
@@ -190,7 +189,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||
* @return BelongsToMany<Organization, $this, Member, 'membership'>
|
||||
*/
|
||||
public function ownedOrganizations(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -25,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\Forms\Components\Section;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
|
||||
@@ -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 Filament\Http\Middleware\Authenticate;
|
||||
use App\Http\Middleware\AuthenticateFilamentPanel;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\NavigationGroup;
|
||||
use Filament\Pages;
|
||||
use Filament\Pages\Dashboard;
|
||||
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\VerifyCsrfToken;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
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([
|
||||
Pages\Dashboard::class,
|
||||
Dashboard::class,
|
||||
])
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||
->widgets([
|
||||
@@ -51,7 +51,6 @@ class AdminPanelProvider extends PanelProvider
|
||||
TimeEntriesCreated::class,
|
||||
TimeEntriesImported::class,
|
||||
])
|
||||
->viteTheme('resources/css/filament/admin/theme.css')
|
||||
->plugins([
|
||||
EnvironmentIndicatorPlugin::make()
|
||||
->color(fn () => match (App::environment()) {
|
||||
@@ -72,6 +71,9 @@ class AdminPanelProvider extends PanelProvider
|
||||
NavigationGroup::make()
|
||||
->label('Auth')
|
||||
->collapsed(),
|
||||
NavigationGroup::make()
|
||||
->label('Self Hosting')
|
||||
->collapsed(),
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
@@ -79,13 +81,13 @@ class AdminPanelProvider extends PanelProvider
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
PreventRequestForgery::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
AuthenticateFilamentPanel::class,
|
||||
]);
|
||||
|
||||
$modules = Module::allEnabled();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,18 @@ use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Log;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Throwable;
|
||||
|
||||
class ApiService
|
||||
{
|
||||
private const string API_URL = 'https://app.solidtime.io/api/v1';
|
||||
|
||||
private function isPremium(): bool
|
||||
{
|
||||
return Module::has('Invoicing');
|
||||
}
|
||||
|
||||
public function checkForUpdate(): ?string
|
||||
{
|
||||
try {
|
||||
@@ -30,6 +37,7 @@ class ApiService
|
||||
'version' => config('app.version'),
|
||||
'build' => config('app.build'),
|
||||
'url' => config('app.url'),
|
||||
'is_premium' => $this->isPremium(),
|
||||
]);
|
||||
|
||||
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
|
||||
@@ -42,7 +50,7 @@ class ApiService
|
||||
|
||||
return null;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('Failed to check for update', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
@@ -61,6 +69,7 @@ class ApiService
|
||||
'version' => config('app.version'),
|
||||
'build' => config('app.build'),
|
||||
'url' => config('app.url'),
|
||||
'is_premium' => $this->isPremium(),
|
||||
// telemetry data
|
||||
'user_count' => User::count(),
|
||||
'organization_count' => Organization::count(),
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\Money;
|
||||
|
||||
class CurrencyService
|
||||
@@ -378,7 +378,7 @@ class CurrencyService
|
||||
|
||||
public function getRandomCurrencyCode(): string
|
||||
{
|
||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencyCodes = array_keys($currencies);
|
||||
|
||||
return $currencyCodes[array_rand($currencyCodes)];
|
||||
|
||||
@@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class DashboardService
|
||||
{
|
||||
@@ -101,7 +102,7 @@ class DashboardService
|
||||
$value1 = Carbon::createFromFormat('Y-m-d', $possibleDates->first(), $timeZone);
|
||||
$value2 = Carbon::createFromFormat('Y-m-d', $possibleDates->last(), $timeZone);
|
||||
if ($value2 === null || $value1 === null) {
|
||||
throw new \RuntimeException('Provided date is not valid');
|
||||
throw new RuntimeException('Provided date is not valid');
|
||||
}
|
||||
if ($value1->gt($value2)) {
|
||||
$last = $value1;
|
||||
@@ -151,12 +152,13 @@ class DashboardService
|
||||
|
||||
$possibleDays = $this->lastDays($days, $timezone);
|
||||
|
||||
// The SQL fragment contains only a timezone offset calculated as an integer.
|
||||
$query = TimeEntry::query()
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
|
||||
->where('user_id', '=', $user->getKey())
|
||||
->where('organization_id', '=', $organization->getKey())
|
||||
->workTime()
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
|
||||
->orderBy('date');
|
||||
|
||||
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
||||
@@ -193,12 +195,13 @@ class DashboardService
|
||||
}
|
||||
$possibleDays = $this->daysOfThisWeek($timezone, $user->week_start);
|
||||
|
||||
// The SQL fragment contains only a timezone offset calculated as an integer.
|
||||
$query = TimeEntry::query()
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
|
||||
->where('user_id', '=', $user->getKey())
|
||||
->where('organization_id', '=', $organization->getKey())
|
||||
->workTime()
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
|
||||
->orderBy('date');
|
||||
|
||||
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
||||
|
||||
@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ReportPropertiesDto implements Castable
|
||||
{
|
||||
@@ -101,15 +102,15 @@ class ReportPropertiesDto implements Castable
|
||||
public function get(Model $model, string $key, mixed $value, array $attributes): ReportPropertiesDto
|
||||
{
|
||||
if (! is_string($value)) {
|
||||
throw new \InvalidArgumentException('The given value is not a string');
|
||||
throw new InvalidArgumentException('The given value is not a string');
|
||||
}
|
||||
$data = json_decode($value, false);
|
||||
if ($data === null) {
|
||||
throw new \InvalidArgumentException('The given value is not a JSON string');
|
||||
throw new InvalidArgumentException('The given value is not a JSON string');
|
||||
}
|
||||
foreach (self::REQUIRED_PROPERTIES as $property) {
|
||||
if (! property_exists($data, $property)) {
|
||||
throw new \InvalidArgumentException('The given JSON string does not contain the required property "'.$property.'"');
|
||||
throw new InvalidArgumentException('The given JSON string does not contain the required property "'.$property.'"');
|
||||
}
|
||||
}
|
||||
$dto = new ReportPropertiesDto;
|
||||
@@ -145,7 +146,7 @@ class ReportPropertiesDto implements Castable
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): string
|
||||
{
|
||||
if (! ($value instanceof ReportPropertiesDto)) {
|
||||
throw new \InvalidArgumentException('The given value is not an instance of ReportPropertiesDto');
|
||||
throw new InvalidArgumentException('The given value is not an instance of ReportPropertiesDto');
|
||||
}
|
||||
|
||||
$data = (object) [
|
||||
@@ -171,7 +172,7 @@ class ReportPropertiesDto implements Castable
|
||||
|
||||
$jsonString = json_encode($data);
|
||||
if ($jsonString === false) {
|
||||
throw new \InvalidArgumentException('Could not encode the given data to a JSON string');
|
||||
throw new InvalidArgumentException('Could not encode the given data to a JSON string');
|
||||
}
|
||||
|
||||
return $jsonString;
|
||||
@@ -188,10 +189,10 @@ class ReportPropertiesDto implements Castable
|
||||
$collection = new Collection;
|
||||
foreach ($ids as $id) {
|
||||
if (! is_string($id)) {
|
||||
throw new \InvalidArgumentException('The given ID is not a string');
|
||||
throw new InvalidArgumentException('The given ID is not a string');
|
||||
}
|
||||
if ($id !== TimeEntryFilter::NONE_VALUE && ! Str::isUuid($id)) {
|
||||
throw new \InvalidArgumentException('The given ID is not a valid UUID');
|
||||
throw new InvalidArgumentException('The given ID is not a valid UUID');
|
||||
}
|
||||
$collection->push($id);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @template TModel of Model
|
||||
@@ -133,7 +134,7 @@ class ImportDatabaseHelper
|
||||
{
|
||||
$jsonData = json_encode($data);
|
||||
if ($jsonData === false) {
|
||||
throw new \RuntimeException('Failed to encode data to JSON');
|
||||
throw new RuntimeException('Failed to encode data to JSON');
|
||||
}
|
||||
|
||||
return md5($jsonData);
|
||||
@@ -164,7 +165,7 @@ class ImportDatabaseHelper
|
||||
|
||||
return $this->createEntity($identifierData, $createValues, $externalIdentifier);
|
||||
} else {
|
||||
throw new \RuntimeException('Not implemented');
|
||||
throw new RuntimeException('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Csv\Exception as CsvException;
|
||||
use League\Csv\Reader;
|
||||
use Override;
|
||||
|
||||
class ClockifyProjectsImporter extends DefaultImporter
|
||||
{
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function importData(string $data, string $timezone): void
|
||||
{
|
||||
try {
|
||||
@@ -129,13 +130,13 @@ class ClockifyProjectsImporter extends DefaultImporter
|
||||
return $billableRateKey;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return __('importer.clockify_projects.name');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return __('importer.clockify_projects.description');
|
||||
|
||||
@@ -15,6 +15,7 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Csv\Exception as CsvException;
|
||||
use League\Csv\Reader;
|
||||
use Override;
|
||||
|
||||
class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
{
|
||||
@@ -44,7 +45,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function importData(string $data, string $timezone): void
|
||||
{
|
||||
try {
|
||||
@@ -263,13 +264,13 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
return null;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return __('importer.clockify_time_entries.name');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return __('importer.clockify_time_entries.description');
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Service\ColorService;
|
||||
use App\Service\Import\ImportDatabaseHelper;
|
||||
use App\Service\TimezoneService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Override;
|
||||
|
||||
abstract class DefaultImporter implements ImporterContract
|
||||
{
|
||||
@@ -193,7 +194,7 @@ abstract class DefaultImporter implements ImporterContract
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getReport(): ReportDto
|
||||
{
|
||||
return new ReportDto(
|
||||
|
||||
@@ -14,6 +14,7 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Csv\Exception as CsvException;
|
||||
use League\Csv\Reader;
|
||||
use Override;
|
||||
|
||||
class GenericTimeEntriesImporter extends DefaultImporter
|
||||
{
|
||||
@@ -59,7 +60,7 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function importData(string $data, string $timezone): void
|
||||
{
|
||||
try {
|
||||
@@ -195,13 +196,13 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return __('importer.generic_time_entries.name');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return __('importer.generic_time_entries.description');
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Service\Import\Importers;
|
||||
use Exception;
|
||||
use League\Csv\Exception as CsvException;
|
||||
use League\Csv\Reader;
|
||||
use Override;
|
||||
|
||||
class HarvestClientsImporter extends DefaultImporter
|
||||
{
|
||||
@@ -20,7 +21,7 @@ class HarvestClientsImporter extends DefaultImporter
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function importData(string $data, string $timezone): void
|
||||
{
|
||||
try {
|
||||
@@ -62,13 +63,13 @@ class HarvestClientsImporter extends DefaultImporter
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return __('importer.harvest_clients.name');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return __('importer.harvest_clients.description');
|
||||
|
||||
@@ -8,6 +8,7 @@ use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Csv\Exception as CsvException;
|
||||
use League\Csv\Reader;
|
||||
use Override;
|
||||
|
||||
class HarvestProjectsImporter extends DefaultImporter
|
||||
{
|
||||
@@ -24,7 +25,7 @@ class HarvestProjectsImporter extends DefaultImporter
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function importData(string $data, string $timezone): void
|
||||
{
|
||||
try {
|
||||
@@ -93,13 +94,13 @@ class HarvestProjectsImporter extends DefaultImporter
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return __('importer.harvest_projects.name');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return __('importer.harvest_projects.description');
|
||||
|
||||
@@ -4,4 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Import\Importers;
|
||||
|
||||
class ImportException extends \Exception {}
|
||||
use Exception;
|
||||
|
||||
class ImportException extends Exception {}
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Import\Importers;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ImporterProvider
|
||||
{
|
||||
/**
|
||||
@@ -49,7 +51,7 @@ class ImporterProvider
|
||||
public function getImporter(string $type): ImporterContract
|
||||
{
|
||||
if (! array_key_exists($type, $this->importers)) {
|
||||
throw new \InvalidArgumentException('Invalid importer type');
|
||||
throw new InvalidArgumentException('Invalid importer type');
|
||||
}
|
||||
|
||||
return new $this->importers[$type];
|
||||
|
||||
@@ -14,6 +14,7 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Csv\Exception as CsvException;
|
||||
use League\Csv\Reader;
|
||||
use Override;
|
||||
|
||||
class TogglTimeEntriesImporter extends DefaultImporter
|
||||
{
|
||||
@@ -43,7 +44,7 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function importData(string $data, string $timezone): void
|
||||
{
|
||||
try {
|
||||
@@ -193,13 +194,13 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return __('importer.toggl_time_entries.name');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function getDescription(): string
|
||||
{
|
||||
return __('importer.toggl_time_entries.description');
|
||||
|
||||
@@ -14,6 +14,7 @@ use League\Csv\CannotInsertRecord;
|
||||
use League\Csv\Exception;
|
||||
use League\Csv\UnavailableStream;
|
||||
use League\Csv\Writer;
|
||||
use LogicException;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
/**
|
||||
@@ -116,7 +117,7 @@ abstract class CsvExport
|
||||
private function validateRow(array $row): void
|
||||
{
|
||||
if (array_keys($row) !== static::HEADER) {
|
||||
throw new \LogicException('Invalid row');
|
||||
throw new LogicException('Invalid row');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TimeEntryAggregationService
|
||||
{
|
||||
@@ -74,8 +75,9 @@ class TimeEntryAggregationService
|
||||
$startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
||||
$endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes);
|
||||
|
||||
// SQL fragments come from enum cases, integer offsets, and formatted dates.
|
||||
$timeEntriesQuery->selectRaw(
|
||||
($group1Select !== null ? $group1Select.' as group_1,' : '').
|
||||
($group1Select !== null ? $group1Select.' as group_1,' : ''). // @phpstan-ignore argument.type
|
||||
($group2Select !== null ? $group2Select.' as group_2,' : '').
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||
@@ -104,7 +106,7 @@ class TimeEntryAggregationService
|
||||
$baseTotalsPerGroup1Query = $baseTotalsQuery->clone();
|
||||
$baseTotalsPerGroup1 = $baseTotalsPerGroup1Query
|
||||
->selectRaw(
|
||||
$group1Select.' as group_1,'.
|
||||
$group1Select.' as group_1,'. // @phpstan-ignore argument.type
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||
)
|
||||
@@ -169,7 +171,7 @@ class TimeEntryAggregationService
|
||||
// Reset selects and ordering on the cloned base query
|
||||
$baseTotals = $baseTotalsQuery
|
||||
->selectRaw(
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. // @phpstan-ignore argument.type
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||
)
|
||||
->first();
|
||||
@@ -526,7 +528,7 @@ class TimeEntryAggregationService
|
||||
public function timeSlotsBetween(Carbon $start, Carbon $end, string $timezone, Weekday $startOfWeek, TimeEntryAggregationTypeInterval $interval, string $format): Collection
|
||||
{
|
||||
if ($start->gt($end)) {
|
||||
throw new \InvalidArgumentException('Start date must be before end date');
|
||||
throw new InvalidArgumentException('Start date must be before end date');
|
||||
}
|
||||
$slots = new Collection;
|
||||
$current = $start->copy()->timezone($timezone);
|
||||
@@ -539,7 +541,7 @@ class TimeEntryAggregationService
|
||||
} elseif ($interval === TimeEntryAggregationTypeInterval::Year) {
|
||||
$current->startOfYear();
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid interval');
|
||||
throw new InvalidArgumentException('Invalid interval');
|
||||
}
|
||||
|
||||
while ($current->lt($end)) {
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Service;
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonTimeZone;
|
||||
use Exception;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -287,7 +288,7 @@ class TimezoneService
|
||||
{
|
||||
try {
|
||||
return new CarbonTimeZone($user->timezone);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
Log::error('User has a invalid timezone', [
|
||||
'user_id' => $user->getKey(),
|
||||
'timezone' => $user->timezone,
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class UserService
|
||||
{
|
||||
@@ -148,7 +149,7 @@ class UserService
|
||||
->whereBelongsTo($newOwner, 'user')
|
||||
->first();
|
||||
if ($userMembership === null) {
|
||||
throw new \InvalidArgumentException('User is not a member of the organization');
|
||||
throw new InvalidArgumentException('User is not a member of the organization');
|
||||
}
|
||||
$userMembership->role = Role::Owner->value;
|
||||
$userMembership->save();
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use finfo;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
|
||||
class Base64File
|
||||
@@ -27,7 +28,7 @@ class Base64File
|
||||
return null;
|
||||
}
|
||||
|
||||
$mimeType = (new \finfo(FILEINFO_MIME_TYPE))->buffer($decoded);
|
||||
$mimeType = (new finfo(FILEINFO_MIME_TYPE))->buffer($decoded);
|
||||
if ($mimeType === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,34 +7,34 @@
|
||||
"require": {
|
||||
"php": "8.3.*",
|
||||
"ext-zip": "*",
|
||||
"brick/money": "^0.10.0",
|
||||
"brick/money": "^0.15.1",
|
||||
"datomatic/laravel-enum-helper": "^2.0.0",
|
||||
"dedoc/scramble": "^0.12.2",
|
||||
"filament/filament": "^3.2",
|
||||
"flowframe/laravel-trend": "^0.4.0",
|
||||
"dedoc/scramble": "^0.13.43",
|
||||
"filament/filament": "^5.0",
|
||||
"flowframe/laravel-trend": "^0.5.0",
|
||||
"gotenberg/gotenberg-php": "^2.8",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"inertiajs/inertia-laravel": "^2.0.3",
|
||||
"guzzlehttp/guzzle": "^8.2",
|
||||
"inertiajs/inertia-laravel": "^3.3.4",
|
||||
"korridor/laravel-computed-attributes": "^3.1",
|
||||
"korridor/laravel-has-many-sync": "^3.1",
|
||||
"korridor/laravel-model-validation-rules": "^3.0",
|
||||
"laravel/fortify": "^1.37",
|
||||
"laravel/framework": "^12.19.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/octane": "^2.3",
|
||||
"laravel/passport": "^13.0.5",
|
||||
"laravel/tinker": "^2.8",
|
||||
"laravel/tinker": "^3.0",
|
||||
"league/csv": "^9.16.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/iso3166": "^4.3",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"maatwebsite/excel": "^4.0.3",
|
||||
"mobiledetect/mobiledetectlib": "^4.11",
|
||||
"novadaemon/filament-pretty-json": "^2.2",
|
||||
"nwidart/laravel-modules": "^12.0.4",
|
||||
"novadaemon/filament-pretty-json": "^4.0",
|
||||
"nwidart/laravel-modules": "^13.0.0",
|
||||
"owen-it/laravel-auditing": "^14.0.0",
|
||||
"pxlrbt/filament-environment-indicator": "^2.1.0",
|
||||
"pxlrbt/filament-environment-indicator": "^3.0",
|
||||
"spatie/temporary-directory": "^2.2",
|
||||
"staudenmeir/eloquent-json-relations": "^1.1",
|
||||
"stechstudio/filament-impersonate": "^3.8",
|
||||
"stechstudio/filament-impersonate": "^5.0",
|
||||
"tightenco/ziggy": "^2.1.0",
|
||||
"tpetry/laravel-postgresql-enhanced": "^3.0.0",
|
||||
"wikimedia/composer-merge-plugin": "^2.1.0"
|
||||
|
||||
3526
composer.lock
generated
3526
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,9 @@ return [
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application. This value is used when the
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| any other location as required by the application or its packages.
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -110,8 +110,8 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. We have gone
|
||||
| ahead and set this to a sensible default for you out of the box.
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -123,8 +123,8 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by the translation service provider. You are free to set this value
|
||||
| to any of the locales which will be supported by the application.
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -178,15 +178,21 @@ return [
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is used by the Illuminate encrypter service and should be set
|
||||
| to a random, 32 character string, otherwise these encrypted strings
|
||||
| will not be safe. Please do this before deploying an application!
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
'localization' => [
|
||||
'default_currency' => env('LOCALIZATION_DEFAULT_CURRENCY', 'EUR'),
|
||||
@@ -206,13 +212,13 @@ return [
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
| Supported drivers: "file", "cache", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => 'file',
|
||||
// 'store' => 'redis',
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -10,8 +10,8 @@ return [
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default authentication "guard" and password
|
||||
| reset options for your application. You may change these defaults
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
@@ -28,11 +28,11 @@ return [
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| here which uses session storage and the Eloquent user provider.
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
@@ -55,12 +55,12 @@ return [
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| sources which represent each model / table. These sources may then
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
@@ -80,9 +80,9 @@ return [
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may specify multiple password reset configurations if you have more
|
||||
| than one user table or model in the application and you want to have
|
||||
| separate password reset settings based on the specific user types.
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
@@ -108,13 +108,13 @@ return [
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the amount of seconds before a password confirmation
|
||||
| times out and the user is prompted to re-enter their password via the
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => 10800,
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ return [
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache connection that gets used while
|
||||
| using this caching library. This connection is used when another is
|
||||
| not explicitly specified when executing a given caching function.
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -28,8 +28,9 @@ return [
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "apc", "array", "database", "file",
|
||||
| "memcached", "redis", "dynamodb", "octane", "null"
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "storage", "octane",
|
||||
| "session", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -46,9 +47,10 @@ return [
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'cache',
|
||||
'connection' => null,
|
||||
'lock_connection' => null,
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
@@ -57,6 +59,12 @@ return [
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'storage' => [
|
||||
'driver' => 'storage',
|
||||
'disk' => env('CACHE_STORAGE_DISK'),
|
||||
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
@@ -78,8 +86,8 @@ return [
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'cache',
|
||||
'lock_connection' => 'default',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
@@ -95,6 +103,14 @@ return [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -102,12 +118,24 @@ return [
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, or DynamoDB cache
|
||||
| stores there might be other applications using the same cache. For
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
];
|
||||
|
||||
@@ -12,26 +12,22 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for all database work. Of course
|
||||
| you may use many connections at once using the Database library.
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
'default' => env('DB_CONNECTION', 'pgsql'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here are each of the database connections setup for your application.
|
||||
| Of course, examples of configuring each database platform that is
|
||||
| supported by Laravel is shown below to make development simple.
|
||||
|
|
||||
|
|
||||
| All database work in Laravel is done through the PHP PDO facilities
|
||||
| so make sure you have the driver for your particular database of
|
||||
| choice installed on your machine before you begin development.
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -39,50 +35,34 @@ return [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'pgsql_test' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_TEST_PORT', '5432'),
|
||||
'database' => env('DB_TEST_DATABASE', 'forge'),
|
||||
@@ -92,18 +72,18 @@ return [
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
@@ -119,11 +99,14 @@ return [
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run in the database.
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => 'migrations',
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -132,7 +115,7 @@ return [
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -143,6 +126,7 @@ return [
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
@@ -152,6 +136,10 @@ return [
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
@@ -161,6 +149,10 @@ return [
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user