Сегодня мы рассмотрим увлекательную задачу по разработке веб-приложения для управления персоналом и расчёта заработной платы.
Приложение создано в учебных целях и не предназначено для использования в реальных бизнес-процессах. Все данные, используемые в приложении, являются вымышленными и не имеют отношения к реальным компаниям или сотрудникам.
- Цель проекта
- Данные о сотрудниках
- Требования к функционалу
- Решение задачи
- Настройка окружения
- Создание моделей и миграций
- Настройка миграций
- Миграция Departaments
- Миграция Employees
- Миграция Salaries
- Настройка моделей
- Модель Departament
- Модель Employee
- Модель Salary
- Заполнение БД фейковыми данными
- 1. Создание Factory для департаментов
- 2. Создание Factory для сотрудников
- 3. Создание Factory для зарплат
- 4. Создание Seed для департаментов, сотрудников и зарплат
- 5. Регистрация Seeders
- Создание ресурса
- Устанавливаем конструктор панелей
- Настройка ресурса сотрудников
- Настройка менеджера связей с зарплатой
- Создание сервиса подсчета рабочих дней
- Создание виджетов
- Выгрузка отчетов
- Выгрузка в pdf
- composer require torgodly/html2media
- Заключение
Цель проекта
Основная цель проекта — создать веб-приложение, которое позволит пользователям:
- Группировать данные по подразделениям, сотрудникам и месяцам.
- Осуществлять расчёт итоговых сумм заработной платы для каждого сотрудника и подразделения.
Данные о сотрудниках
Для начала работы необходимо определить структуру данных о сотрудниках. В нашем приложении будут следующие поля:
- Подразделение: название подразделения, к которому относится сотрудник.
- Фамилия, имя, отчество: полные данные о сотруднике.
- Оклад (О): фиксированная сумма заработной платы сотрудника.
- Начисления основной заработной платы: начисления за каждый месяц, включая количество рабочих дней в месяце (М) и количество рабочих дней сотрудника (Д).
Требования к функционалу
Приложение должно поддерживать следующие основные функции:
Расчёт итоговых сумм:
— Автоматический расчёт начисленной заработной платы для каждого подразделения и сотрудника на основе оклада и количества рабочих дней.
— Итоговая сумма заработной платы за каждый месяц для каждого подразделения и сотрудника.
Группировка данных:
— Возможность группировки данных по подразделениям.
— Возможность группировки данных по сотрудникам.
— Возможность группировки данных по месяцам.
Решение задачи
- Настройка окружения:
— Установите PHP и настройте локальное окружение для разработки.
— Установите Laravel 11 и настройте проект. - Создание моделей и миграций:
— Создайте модели данных, которые будут использоваться в приложении.
— Напишите миграции для создания таблиц в базе данных MySQL. - Установка и настройка Filament3:
— Установите Filament3 для работы с шаблонами.
— Настройте шаблоны для отображения данных из базы данных. - Создание виджетов
- Выгрузка отчетов
Настройка окружения
Установим Laravel и филамент
1 2 | composer require filament/filament php artisan filament:install --panels |
Создание моделей и миграций
Для создания файлов с миграциями и моделями выполним в консоли следующие команды:
1 2 3 | php artisan make:model Department -m php artisan make:model Employee -m php artisan make:model Salary -m |
Послед создания файлов отредактируем их:
Настройка миграций
Миграция Departaments
1 2 3 4 5 | Schema::create('departments', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); |
Миграция Employees
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Schema::create('employees', function (Blueprint $table) { $table->id(); $table->string('full_name'); $table->date('date_of_birth'); $table->string('image')->nullable(); // Image field, can be nullable $table->string('address'); $table->string('contact_number'); $table->string('position_title'); $table->foreignId('department_id')->constrained('departments')->onDelete('cascade'); $table->date('start_date'); $table->string('employment_status'); $table->decimal('salary', 10, 2); // Decimal for salary $table->timestamps(); }); |
Миграция Salaries
1 2 3 4 5 6 7 8 9 10 | Schema::create('salaries', function (Blueprint $table) { $table->id(); $table->foreignId('employee_id')->constrained('employees')->onDelete('cascade'); $table->decimal('monthly_salary', 10, 2); $table->string('month'); $table->string('year'); $table->integer('working_days_in_month')->nullable(); // Количество рабочих дней в месяце $table->integer('worked_days')->nullable(); // Количество отработанных дней $table->timestamps(); }); |
Настройка моделей
Модель Departament
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Department extends Model { use HasFactory; public function employees() { return $this->hasMany(Employee::class); } public function salaries(): HasManyThrough { return $this->hasManyThrough(Salary::class, Employee::class); } } |
Модель Employee
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Employee extends Model { use HasFactory; protected $fillable = [ 'employee_id', 'full_name', 'image', 'address', 'contact_number', 'date_of_birth', 'position_title', 'department_id', 'start_date', 'salary', ]; public function department() { return $this->belongsTo(Department::class); } public function salaries() { return $this->hasMany(Salary::class); } } |
Модель Salary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Salary extends Model { use HasFactory; public function employee() { return $this->belongsTo(Employee::class); } public function department() { return $this->hasOneThrough(Department::class, Employee::class); } } |
Заполнение БД фейковыми данными
Для создания seed и factory для моделей Department
, Employee
и Salary
в Laravel, следуйте приведенным ниже шагам.
1. Создание Factory для департаментов
Создайте factory для модели Department
, чтобы генерировать случайные названия департаментов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | use App\Models\Department; use Illuminate\Database\Eloquent\Factories\Factory; class DepartmentFactory extends Factory { protected $model = Department::class; public function definition() { return [ 'name' => $this->faker->unique()->word, // Генерируем уникальное название департамента ]; } } |
2. Создание Factory для сотрудников
Теперь создадим factory для модели Employee
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | use App\Models\Employee; use Illuminate\Database\Eloquent\Factories\Factory; class EmployeeFactory extends Factory { protected $model = Employee::class; public function definition() { return [ 'full_name' => $this->faker->name, 'date_of_birth' => $this->faker->date(), 'image' => $this->faker->imageUrl(), 'address' => $this->faker->address, 'contact_number' => $this->faker->phoneNumber, 'position_title' => $this->faker->jobTitle, 'department_id' => Department::factory(), // Создаем департамент для сотрудника 'start_date' => $this->faker->date(), 'employment_status' => $this->faker->randomElement(['active', 'inactive']), 'salary' => $this->faker->randomFloat(2, 30000, 100000), ]; } } |
3. Создание Factory для зарплат
Теперь создадим factory для модели Salary
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | use App\Models\Salary; use Illuminate\Database\Eloquent\Factories\Factory; class SalaryFactory extends Factory { protected $model = Salary::class; public function definition() { return [ 'employee_id' => Employee::factory(), // Создаем сотрудника для зарплаты 'monthly_salary' => $this->faker->randomFloat(2, 3000, 10000), 'month' => $this->faker->month, 'year' => $this->faker->year, ]; } } |
4. Создание Seed для департаментов, сотрудников и зарплат
Теперь создадим seed, который будет использовать эти factories для создания департаментов, сотрудников и их зарплат.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <?php namespace Database\Seeders; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use App\Models\Department; use App\Models\Employee; use App\Models\Salary; use Carbon\Carbon; class DatabaseSeeder extends Seeder { /** * Seed the application's database. */ public function run(): void { // Создаем 5 департаментов $departments = Department::factory(5)->create(); // Создаем 10 сотрудников и генерируем зарплаты для каждого $employees = Employee::factory(10)->create(); foreach ($employees as $employee) { // Генерируем зарплаты для каждого сотрудника на 1-5 лет $endYear = now()->year; $startYear = $endYear - rand(1, 5); for ($year = $startYear; $year <= $endYear; $year++) { for ($month = 1; $month <= 12; $month++) { $startDate = Carbon::create($year, $month, 1); $endDate = $startDate->copy()->endOfMonth(); // Подсчет рабочих дней (будние дни) $workingDaysInMonth = $startDate->diffInDaysFiltered(function (Carbon $date) { return !$date->isWeekend(); // Фильтруем только будние дни }, $endDate); $workingDays = rand($workingDaysInMonth - 6, $workingDaysInMonth); Salary::create([ 'employee_id' => $employee->id, 'working_days_in_month' => $workingDaysInMonth, 'worked_days' => $workingDays, 'monthly_salary' => $employee->salary*$workingDays/$workingDaysInMonth, 'month' => $month, 'year' => $year, ]); } } } } } |
5. Регистрация Seeders
Не забудьте зарегистрировать ваш seeder в DatabaseSeeder.php
, если вы еще этого не сделали:
php artisan migrate --seed
Создание ресурса
Устанавливаем конструктор панелей
Устанавливаем конструктор панелей Filament, выполнив следующие команды в каталоге вашего проекта Laravel:
1 2 | composer require filament/filament -W php artisan filament:install --panels |
Вы можете создать новую учетную запись пользователя с помощью следующей команды:
1 | php artisan make:filament-user |
Настройка ресурса сотрудников
1 | php artisan make:filament-resource Employee --generate --view |
EmployeeResource.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | <?php namespace App\Filament\Resources; use Filament\Forms; use Filament\Tables; use App\Models\Employee; use Filament\Forms\Form; use Filament\Tables\Table; use Filament\Infolists\Infolist; use Filament\Resources\Resource; use Filament\Resources\Pages\Page; use Filament\Tables\Columns\TextColumn; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\FileUpload; use Filament\Pages\SubNavigationPosition; use Illuminate\Database\Eloquent\Builder; use Filament\Infolists\Components\Section; use Filament\Infolists\Components\TextEntry; use App\Filament\Resources\EmployeeResource\Pages; use Illuminate\Database\Eloquent\SoftDeletingScope; use App\Filament\Resources\EmployeeResource\RelationManagers; use Filament\Tables\Columns\ImageColumn; class EmployeeResource extends Resource { protected static ?string $model = Employee::class; protected static SubNavigationPosition $subNavigationPosition = SubNavigationPosition::Top; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('full_name') ->required() ->maxLength(255), Forms\Components\DatePicker::make('date_of_birth') ->required(), Forms\Components\FileUpload::make('image') ->image(), Forms\Components\TextInput::make('address') ->required() ->maxLength(255), Forms\Components\TextInput::make('contact_number') ->required() ->maxLength(255), Forms\Components\TextInput::make('position_title') ->required() ->maxLength(255), Forms\Components\TextInput::make('department_id') ->required() ->numeric(), Forms\Components\DatePicker::make('start_date') ->required(), Forms\Components\TextInput::make('employment_status') ->required() ->maxLength(255), Forms\Components\TextInput::make('salary') ->required() ->numeric(), ]); } public static function infolist(Infolist $infolist): Infolist { return $infolist ->schema([ Section::make('State Info') ->schema([ TextEntry::make('position_title')->label('Position title'), TextEntry::make('full_name')->label('Full Name'), ])->columns(2) ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('full_name') ->searchable(), Tables\Columns\TextColumn::make('date_of_birth') ->date() ->sortable(), Tables\Columns\ImageColumn::make('image'), Tables\Columns\TextColumn::make('address') ->searchable(), Tables\Columns\TextColumn::make('contact_number') ->searchable(), Tables\Columns\TextColumn::make('position_title') ->searchable(), Tables\Columns\TextColumn::make('department_id') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('start_date') ->date() ->sortable(), Tables\Columns\TextColumn::make('employment_status') ->searchable(), Tables\Columns\TextColumn::make('salary') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ // ]) ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } public static function getRelations(): array { return [ // ]; } public static function getRecordSubNavigation(Page $page): array { return $page->generateNavigationItems([ Pages\ViewEmploee::class, Pages\EditEmployee::class, Pages\ManageEmployeeSalary::class, ]); } public static function getPages(): array { return [ 'index' => Pages\ListEmployees::route('/'), 'create' => Pages\CreateEmployee::route('/create'), 'edit' => Pages\EditEmployee::route('/{record}/edit'), 'view' => Pages\ViewEmploee::route('/{record}'), 'comments' => Pages\ManageEmployeeSalary::route('/{record}/salaries'), ]; } } |
Настройка менеджера связей с зарплатой
Использование страницы ManageRelatedRecords является альтернативой использованию менеджера связей, если вы хотите отделить функциональность управления отношениями от редактирования или просмотра записи о владельце.
Эта функция идеальна, если вы используете вспомогательную навигацию по ресурсам, так как вы можете легко переключаться между страницей просмотра или редактирования и страницей связей.
Чтобы создать страницу отношений, вам следует использовать команду make:filament-page:
1 | php artisan make:filament-page ManageEmployeeSalary --resource=EmployeeResource --type=ManageRelatedRecords |
app\Filament\Resources\EmployeeResource\Pages\ManageEmployeeSalary.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | <?php namespace App\Filament\Resources\EmployeeResource\Pages; use App\Filament\Resources\Blog\PostResource; use App\Filament\Resources\EmployeeResource; use App\Models\Salary; use Filament\Forms; use Filament\Forms\Form; use Filament\Infolists\Components\IconEntry; use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Infolist; use Filament\Resources\Pages\ManageRelatedRecords; use Filament\Tables; use Filament\Tables\Columns\Summarizers\Sum; use Filament\Tables\Table; use Illuminate\Contracts\Support\Htmlable; use Filament\Tables\Enums\FiltersLayout; class ManageEmployeeSalary extends ManageRelatedRecords { protected static string $resource = EmployeeResource::class; protected static string $relationship = 'salaries'; protected static ?string $navigationIcon = 'heroicon-o-currency-dollar'; public function getBreadcrumb(): string { return 'Salary'; } public static function getNavigationLabel(): string { return 'Manage Salary'; } public function form(Form $form): Form { return $form ->schema([]) ->columns(1); } public function table(Table $table): Table { return $table ->recordTitleAttribute('full_name') ->columns([ Tables\Columns\TextColumn::make('year') ->label('Year') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('month') ->label('Month') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('monthly_salary') ->label('Monthly salary') ->summarize(Sum::make()->numeric()) ->searchable() ->sortable(), Tables\Columns\TextColumn::make('employee.full_name') ->label('Employee') ->searchable() ->sortable(), ]) ->paginated([12, 24, 46, 58, 'all']) ->defaultPaginationPageOption(12) ->filters([ Tables\Filters\SelectFilter::make('year') ->options(function () { $years = Salary::selectRaw('year as year') ->distinct() ->pluck('year') ->toArray(); return array_combine($years, $years); }) ], layout: FiltersLayout::AboveContent) ->headerActions([ Tables\Actions\CreateAction::make(), ]) ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ]); } } |
1 |
Создание сервиса подсчета рабочих дней
Создадим класс WorkingDayCounterService
в папке app/Services
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php namespace App\Services; use Carbon\Carbon; use Carbon\CarbonPeriod; use Illuminate\Support\Collection; class WorkingDayCounterService { public function count(int $month, int $year): int { $firstDayOfMonth = Carbon::createFromDate($year, $month, 1); $lastDayOfMonth = $firstDayOfMonth->copy()->endOfMonth(); $period = CarbonPeriod::between($firstDayOfMonth, $lastDayOfMonth); return Collection::make($period)->filter(function ($date) { return !$date->isWeekend(); })->count(); } } |
Создание виджетов
Виджет зарплаты по месяцам
1 | php artisan make:filament-widget EmplSalaryChart --chart |
EmployeeSalary.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | <?php namespace App\Filament\Widgets; use App\Models\Salary; use Carbon\Carbon; use Filament\Widgets\ChartWidget; class EmplSalaryChart extends ChartWidget { protected static ?string $heading = 'Chart'; protected function getData(): array { $activeFilter = $this->filter; // Определяем начало и конец года //$startDate = Carbon::createFromDate($activeFilter)->startOfYear(); //$endDate = Carbon::createFromDate($activeFilter)->endOfYear(); // Получаем сгруппированные данные $data = Salary::where('year', $activeFilter ) ->selectRaw('month as month, sum(monthly_salary) as salary') ->groupBy('month') ->orderBy('month') ->get(); return [ 'datasets' => [ [ 'label' => 'Print jobs', 'data' => $data->map(fn ($value) => $value->salary), 'backgroundColor' => '#36A2EB', 'borderColor' => '#9BD0F5', ], ], 'labels' => $data->map(fn ($value) => $value->month), ]; } protected function getFilters(): ?array { // Получаем уникальные годы из столбца date $years = Salary::selectRaw('Year as year') ->orderBy('year') ->distinct() ->pluck('year') ->toArray(); // Формируем массив для фильтра return array_combine($years, $years); } protected function getType(): string { return 'bar'; } } |
Виджет табличный с группировкой по отделам
1 | php artisan make:filament-widget GroupDepartment --table |
GroupDepartment.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | <?php namespace App\Filament\Widgets; use App\Models\Department; use App\Models\Salary; use Filament\Forms\Components\Builder; use Filament\Tables; use Filament\Tables\Columns\Summarizers\Count; use Filament\Tables\Columns\Summarizers\Sum; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Filament\Widgets\TableWidget as BaseWidget; class GroupDepartment extends BaseWidget { protected int | string | array $columnSpan = 'full'; protected static ?int $sort = 1; public function table(Table $table): Table { return $table ->query( Salary::query() ) ->columns([ TextColumn::make('employee_id') ->summarize( Count::make()->query(fn ($query) => $query->distinct('employee_id')), ) ->label('Employee Count'), TextColumn::make('monthly_salary') ->summarize(Sum::make()), ]) ->defaultGroup('employee.department.name') ->groupsOnly() ->filters([ SelectFilter::make('year') ->options(function(){ $years = Salary::selectRaw('Year as year') ->orderBy('year') ->distinct() ->pluck('year') ->toArray(); // Формируем массив для фильтра return array_combine($years, $years); }) ]) ; } } |
Выгрузка отчетов
Выгрузка в pdf
composer require torgodly/html2media
Html2Media — это мощный пакет Laravel Filament, который позволяет создавать PDF-файлы, просматривать документы и печатать содержимое непосредственно из вашего приложения. 🚀
Установим пакет и добавим в ресурс кнопку print. Так же для отчета нужно будет создать blade шаблон и передать в него данные
Заключение
В результате выполнения проекта было разработано веб-приложение, которое позволяет управлять персоналом и рассчитывать заработную плату. Приложение поддерживает группировку данных по подразделениям, сотрудникам и месяцам, а также автоматический расчёт начисленной заработной платы и итоговых сумм.
Хотя приложение создано в учебных целях и не предназначено для использования в реальных бизнес-процессах, оно может служить примером того, как можно реализовать функционал расчёта заработной платы в веб-приложении. В реальных проектах необходимо учитывать дополнительные требования и ограничения, связанные с законодательством, внутренними политиками компании и другими факторами.