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',