From 0237c2b9efe7f705163df657cbf288b43f9f4ed7 Mon Sep 17 00:00:00 2001 From: Mekan1206 Date: Sun, 2 Aug 2026 22:31:59 +0500 Subject: [PATCH] Enhance ActivityLogsTable by updating event and causer filters to utilize the Activity model directly. Introduce a new causer_id filter with improved query logic for better user experience. Add new methods in EmployeeStatisticsService for applying table aggregates, and update localization files to include new terms for employee statistics in English, Russian, and Turkmen. --- .../ActivityLogs/Tables/ActivityLogsTable.php | 22 +++- .../EmployeeStats/EmployeeStatsResource.php | 119 ++++++++++++++++++ .../EmployeeStats/Pages/ListEmployeeStats.php | 18 +++ .../Tables/EmployeeStatsTable.php | 106 ++++++++++++++++ .../Employee/EmployeeStatisticsService.php | 21 ++++ lang/en/hr.php | 2 + lang/ru/hr.php | 2 + lang/tk/hr.php | 2 + 8 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 app/Filament/Resources/EmployeeStats/EmployeeStatsResource.php create mode 100644 app/Filament/Resources/EmployeeStats/Pages/ListEmployeeStats.php create mode 100644 app/Filament/Resources/EmployeeStats/Tables/EmployeeStatsTable.php diff --git a/app/Filament/Resources/ActivityLogs/Tables/ActivityLogsTable.php b/app/Filament/Resources/ActivityLogs/Tables/ActivityLogsTable.php index 643d027..d1e2ee7 100644 --- a/app/Filament/Resources/ActivityLogs/Tables/ActivityLogsTable.php +++ b/app/Filament/Resources/ActivityLogs/Tables/ActivityLogsTable.php @@ -17,6 +17,7 @@ use Filament\Tables\Filters\Filter; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Spatie\Activitylog\Models\Activity; class ActivityLogsTable { @@ -67,16 +68,29 @@ class ActivityLogsTable ]), SelectFilter::make('event') ->label(__('hr.fields.event')) - ->options(fn (): array => \Spatie\Activitylog\Models\Activity::query() + ->options(fn (): array => Activity::query() ->whereNotNull('event') ->distinct() ->pluck('event', 'event') ->all()), - SelectFilter::make('causer') + SelectFilter::make('causer_id') ->label(__('hr.fields.causer')) - ->relationship('causer', 'name') + ->options(fn (): array => User::query() + ->whereIn('id', Activity::query() + ->where('causer_type', User::class) + ->whereNotNull('causer_id') + ->distinct() + ->pluck('causer_id')) + ->orderBy('name') + ->pluck('name', 'id') + ->all()) ->searchable() - ->preload(), + ->query(fn (Builder $query, array $data): Builder => $query->when( + $data['value'] ?? null, + fn (Builder $query, $causerId): Builder => $query + ->where('causer_id', $causerId) + ->where('causer_type', User::class), + )), Filter::make('created_at') ->label(__('hr.fields.date')) ->schema([ diff --git a/app/Filament/Resources/EmployeeStats/EmployeeStatsResource.php b/app/Filament/Resources/EmployeeStats/EmployeeStatsResource.php new file mode 100644 index 0000000..c3ecc77 --- /dev/null +++ b/app/Filament/Resources/EmployeeStats/EmployeeStatsResource.php @@ -0,0 +1,119 @@ + 'employee_stat', + 'plural' => 'stats', + 'navigation' => 'stats', + ]; + } + + protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartBar; + + protected static string|UnitEnum|null $navigationGroup = NavigationGroup::Employees; + + protected static ?int $navigationSort = 2; + + protected static ?string $slug = 'stats'; + + protected static ?string $recordTitleAttribute = 'full_name'; + + protected static bool $isGloballySearchable = false; + + public static function form(Schema $schema): Schema + { + return $schema; + } + + public static function infolist(Schema $schema): Schema + { + return $schema; + } + + public static function table(Table $table): Table + { + return EmployeeStatsTable::configure($table); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => ListEmployeeStats::route('/'), + ]; + } + + public static function canViewAny(): bool + { + return Gate::check('viewAny', Employee::class); + } + + public static function canView(Model $record): bool + { + return Gate::check('view', $record); + } + + public static function canCreate(): bool + { + return false; + } + + public static function canEdit(Model $record): bool + { + return false; + } + + public static function canDelete(Model $record): bool + { + return false; + } + + public static function canForceDelete(Model $record): bool + { + return false; + } + + public static function canRestore(Model $record): bool + { + return false; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/EmployeeStats/Pages/ListEmployeeStats.php b/app/Filament/Resources/EmployeeStats/Pages/ListEmployeeStats.php new file mode 100644 index 0000000..28625d7 --- /dev/null +++ b/app/Filament/Resources/EmployeeStats/Pages/ListEmployeeStats.php @@ -0,0 +1,18 @@ +modifyQueryUsing(fn ($query) => app(EmployeeStatisticsService::class)->applyTableAggregates($query)) + ->columns([ + TextColumn::make('employee_number') + ->label(__('hr.fields.number')) + ->searchable() + ->sortable(), + TextColumn::make('full_name') + ->label(__('hr.fields.full_name')) + ->searchable() + ->sortable(), + TextColumn::make('department.name') + ->label(__('hr.fields.department')) + ->badge() + ->color('info') + ->searchable() + ->sortable(), + TextColumn::make('position.name') + ->label(__('hr.fields.position')) + ->badge() + ->color('primary') + ->searchable() + ->sortable(), + TextColumn::make('hire_date') + ->label(__('hr.fields.hire_date')) + ->date() + ->sortable(), + TextColumn::make('tenure') + ->label(__('hr.fields.tenure')) + ->state(fn (Employee $record): string => $record->hire_date + ? $record->hire_date->diffForHumans(now(), true) + : '—'), + TextColumn::make('annual_vacation_days_taken') + ->label(__('hr.fields.vacation_taken_days')) + ->numeric() + ->sortable(), + TextColumn::make('vacation_remaining') + ->label(__('hr.fields.vacation_remaining_days')) + ->state(fn (Employee $record): int => (int) config('hr.annual_leave_days') - (int) ($record->annual_vacation_days_taken ?? 0)) + ->numeric() + ->color('success'), + TextColumn::make('sick_leaves_count') + ->label(__('hr.fields.sick_leave_records')) + ->numeric() + ->sortable(), + TextColumn::make('unpaid_leaves_count') + ->label(__('hr.fields.unpaid_leave_records')) + ->numeric() + ->sortable(), + TextColumn::make('disciplinary_reports_count') + ->label(__('hr.fields.disciplinary_reports')) + ->numeric() + ->sortable(), + TextColumn::make('explanations_count') + ->label(__('hr.fields.explanations')) + ->numeric() + ->sortable(), + TextColumn::make('bonuses_sum_amount') + ->label(__('hr.fields.total_bonuses')) + ->money('TMT') + ->sortable(), + TextColumn::make('gifts_count') + ->label(__('hr.fields.gifts_received')) + ->numeric() + ->sortable(), + ]) + ->filters([ + SelectFilter::make('department_id') + ->label(__('hr.fields.department')) + ->relationship('department', 'name') + ->searchable() + ->preload(), + SelectFilter::make('position_id') + ->label(__('hr.fields.position')) + ->relationship('position', 'name') + ->searchable() + ->preload(), + SelectFilter::make('employment_status') + ->label(__('hr.fields.employment_status')) + ->options(EmploymentStatus::class), + ]) + ->recordActions([ + ViewAction::make() + ->url(fn (Employee $record): string => EmployeeResource::getUrl('view', ['record' => $record])), + ]); + } +} diff --git a/app/Services/Employee/EmployeeStatisticsService.php b/app/Services/Employee/EmployeeStatisticsService.php index 5c6db1b..ad67861 100644 --- a/app/Services/Employee/EmployeeStatisticsService.php +++ b/app/Services/Employee/EmployeeStatisticsService.php @@ -6,8 +6,10 @@ namespace App\Services\Employee; use App\DTOs\EmployeeSummaryDto; use App\Enums\ApprovalStatus; +use App\Enums\VacationType; use App\Models\Employee; use App\Services\Vacation\VacationBalanceService; +use Illuminate\Database\Eloquent\Builder; class EmployeeStatisticsService { @@ -15,6 +17,25 @@ class EmployeeStatisticsService private readonly VacationBalanceService $vacationBalanceService, ) {} + /** + * @param Builder $query + * @return Builder + */ + public function applyTableAggregates(Builder $query, ?int $year = null): Builder + { + $year ??= (int) now()->year; + + return $query + ->with(['department', 'position']) + ->withCount(['sickLeaves', 'unpaidLeaves', 'disciplinaryReports', 'explanations', 'gifts']) + ->withSum('bonuses', 'amount') + ->withSum(['vacations as annual_vacation_days_taken' => fn (Builder $vacationQuery): Builder => $vacationQuery + ->where('type', VacationType::Annual) + ->where('status', ApprovalStatus::Approved) + ->whereYear('start_date', $year), + ], 'days'); + } + public function summary(Employee $employee): EmployeeSummaryDto { return new EmployeeSummaryDto( diff --git a/lang/en/hr.php b/lang/en/hr.php index 00f1c2c..b7f2f91 100644 --- a/lang/en/hr.php +++ b/lang/en/hr.php @@ -24,6 +24,8 @@ return [ 'shifts' => 'Shifts', 'employee' => 'Employee', 'employees' => 'Employees', + 'employee_stat' => 'Employee Stat', + 'stats' => 'Stats', 'vacation' => 'Vacation', 'vacations' => 'Vacations', 'sick_leave' => 'Sick Leave', diff --git a/lang/ru/hr.php b/lang/ru/hr.php index ade424c..e0230a5 100644 --- a/lang/ru/hr.php +++ b/lang/ru/hr.php @@ -24,6 +24,8 @@ return [ 'shifts' => 'Смены', 'employee' => 'Сотрудник', 'employees' => 'Сотрудники', + 'employee_stat' => 'Статистика сотрудника', + 'stats' => 'Статистика', 'vacation' => 'Отпуск', 'vacations' => 'Отпуска', 'sick_leave' => 'Больничный', diff --git a/lang/tk/hr.php b/lang/tk/hr.php index 8fcbb81..5961345 100644 --- a/lang/tk/hr.php +++ b/lang/tk/hr.php @@ -24,6 +24,8 @@ return [ 'shifts' => 'Smenalar', 'employee' => 'Işgär', 'employees' => 'Işgärler', + 'employee_stat' => 'Işgär statistikasy', + 'stats' => 'Statistika', 'vacation' => 'Dynç alyş', 'vacations' => 'Dynç alyşlar', 'sick_leave' => 'Kesel rugsady',