У меня есть проект, вдохновленный платформой FilamentDaily. Его цель — создать интерактивную страницу, которая учитывает посещаемость сотрудников. В верхней части страницы расположены дни недели, а в строках — сотрудники. Для каждого сотрудника можно выбрать чекбокс, чтобы отметить, был ли он на работе в конкретный день.
Я решил расширить функциональность этого проекта, добавив возможность вызова модального окна при клике на ячейку с датой. В модальном окне пользователь сможет ввести время прихода и ухода сотрудника, а также указать тип дня (рабочий, выходной, больничный и т.д.). Это позволит более детально отслеживать присутствие сотрудников и улучшить учет рабочего времени.


Создание страницы с расписанием
Сначала нам нужно создать Page через консоль вызвав команду artisan.
1 | php artisan make:filament-page Attendance |
Enum виды посещений
Создадим класс перечислений для выбора типа присутствия.
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 | <?php namespace App\Filament\Enums; use Filament\Support\Contracts\HasDescription; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum Status: string implements HasLabel,HasIcon,HasDescription { case DaytimeWork = 'daytimeWork'; case SickLeave = 'sickLeave'; case Weekend = 'weekend'; case Vacation = 'vacation'; public function getLabel(): ?string { return match ($this) { self::DaytimeWork => 'Работа в дневное время', self::SickLeave => 'Временная нетрудоспособность', self::Weekend => 'Выходной день', self::Vacation => 'Ежегодный основной оплачиваемый отпуск', }; } public function icon(): ?string { return match ($this) { self::DaytimeWork => 'Cirila-YA.svg', self::SickLeave => 'Cirila-BB.svg', self::Weekend => 'Cirila-B.svg', self::Vacation => 'Cirila-OT.svg', }; } public function getIcon(): ?string { return match ($this) { self::DaytimeWork => 'heroicon-m-eye', self::SickLeave => 'heroicon-m-x-mark', self::Weekend => 'heroicon-m-check', self::Vacation => 'heroicon-m-eye', }; } public function getDescription(): ?string { return match ($this) { self::DaytimeWork => 'This has not finished being written yet.', self::SickLeave => 'This is ready for a staff member to read.', self::Weekend => 'This has been approved by a staff member and is public on the website.', self::Vacation => 'A staff member has decided this is not appropriate for the website.', }; } } |
Модель посещений
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Presence extends Model { protected $fillable = [ 'user_id', 'description', 'date', 'entry_time', 'exit_time' ]; public $timestamps = false; public function user() { return $this->belongsTo(User::class); } protected $casts = [ 'description'=> Status::class ]; } |
Blade view сетки с посещениями
Сначала нам нужно программно запустить действие по указанию типа дня и времени присутствия. В компоненте Blade мы используем директиву Livewire wire:click
для вызова функции, которая создаст новый элемент:
resources\views\filament\pages\attendance.blade.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 | <x-filament-panels::page> <div class="text-center"> <x-filament::button tag="a" href="#" wire:click="previousMonth"> {{ $dates['previous'] }} </x-filament::button> <x-filament::button tag="a" href="#" color="success"> {{ $dates['current'] }} </x-filament::button> <x-filament::button tag="a" href="#" wire:click="nextMonth"> {{ $dates['next'] }} </x-filament::button> </div> <x-filament-tables::container> <table class="w-full border-separate border border-gray-400 divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <x-filament-tables::header-cell rowspan="2" class="sticky top-0 z-10 bg-gray-50">User</x-filament-tables::header-cell> @foreach ($userAttendance[array_key_first($userAttendance)] as $date => $value) @if (intval($date) <= 15) <x-filament-tables::header-cell class="sticky top-0 z-10 bg-gray-50 text-center w-12 px-1">{{ $date }}</x-filament-tables::header-cell> @endif @endforeach </tr> <tr> @foreach ($userAttendance[array_key_first($userAttendance)] as $date => $value) @if (intval($date) > 15) <x-filament-tables::header-cell>{{ $date }}</x-filament-tables::header-cell> @endif @endforeach </tr> </thead> <tbody class="divide-y divide-gray-200"> @foreach ($userAttendance as $user =>$dates ) <x-filament-tables::row> <x-filament-tables::cell class="text-center py-2" rowspan="2"> {{ $users[$user] }} </x-filament-tables::cell> @foreach ($dates as $date => $value) @if (intval($date) <= 15) <x-filament-tables::cell class="text-center py-2"> @if ($value === null) <x-filament::icon-button icon="heroicon-m-minus" color="danger" wire:click="createPresence({{$user}},{{$date}})" label="New value" /> @else <x-filament::icon-button class="text-center py-2 {{ $value->description->getLabel() === 'Выходной день'? 'bg-gray-200' : 'bg-gray-50' }} " icon="{{ asset($value->description->icon()) }}" color="danger" wire:click="createPresence({{$user}},{{$date}},{{$value['id']}})" label="{{ $value->description->getLabel() }}" /> @endif </x-filament-tables::cell> @endif @endforeach </x-filament-tables::row> <x-filament-tables::row> @foreach ($dates as $date => $value) @if (intval($date) > 15) <x-filament-tables::cell class="text-center py-2"> @if ($value === null) <x-filament::icon-button icon="heroicon-m-minus" color="danger" wire:click="createPresence({{$user}},{{$date}})" label="New value" /> @else <x-filament::icon-button class="text-center py-2 {{ $value->description->getLabel() === 'Выходной день'? 'bg-gray-200' : 'bg-gray-50' }} " icon="{{ asset($value->description->icon()) }}" color="info" wire:click="createPresence({{$user}},{{$date}},{{$value['id']}})" label="{{ $value->description->getLabel() }}" /> @endif </x-filament-tables::cell> @endif @endforeach </x-filament-tables::row> @endforeach </tbody> </table> </x-filament::tables::container> </x-filament-panels::page> |
1 2 3 4 5 6 7 | <x-filament::icon-button class="text-center py-2 {{ $value->description->getLabel() === 'Выходной день'? 'bg-gray-200' : 'bg-gray-50' }} " icon="{{ asset($value->description->icon()) }}" color="danger" wire:click="createPresence({{$user}},{{$date}},{{$value['id']}})" label="{{ $value->description->getLabel() }}" /> |
Компонент Livewire:
Теперь давайте рассмотрим компонент Livewire, который управляет логикой создания посещений:
app\Filament\Pages\Attendance.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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | <?php namespace App\Filament\Pages; use App\Filament\Enums\Status; use App\Models\Presence; use App\Models\User; use Carbon\Carbon; use Filament\Actions\Action; use Filament\Notifications\Notification; use Livewire\Attributes\Url; use Filament\Pages\Page; use Filament\Support\Enums\MaxWidth; use Filament\Forms\Components\Select; use Filament\Forms\Components\TimePicker; class Attendance extends Page { protected static ?string $navigationIcon = 'heroicon-o-document-text'; protected static string $view = 'filament.pages.attendance'; #[Url] public ?string $selectedDate = null; public array $userAttendance = []; public function mount() { if(!$this->selectedDate){ $this->selectedDate = now(); } } protected function getViewData(): array { return [ 'dates' => $this->getDates(), 'users' => $this->getUsers(), 'userAttendance' => $this->getUserAttendance(), ]; } public function getDates():array { $currentDate = Carbon::parse($this->selectedDate); return [ 'previous' => $currentDate->copy()->subMonthNoOverflow()->format('F Y'), 'current' => $currentDate->format('F Y'), 'next' => $currentDate->copy()->addMonthNoOverflow()->format('F Y'), ]; } public function previousMonth():void { $this->selectedDate = Carbon::parse($this->selectedDate)->subMonthNoOverflow(); } public function nextMonth():void { $this->selectedDate = Carbon::parse($this->selectedDate)->addMonthNoOverflow(); } private function getUsers() { return User::pluck('name','id'); } private function getUserAttendance():array { $start = Carbon::parse($this->selectedDate)->copy()->startOfMonth(); $end = Carbon::parse($this->selectedDate)->copy()->endOfMonth(); $users = User::query() ->with([ 'presences' => function($q) use ($start,$end){ return $q->whereBetween('date',[$start,$end]); } ]) ->get(); $details = []; foreach ($users as $user){ $presencesByDate = $user->presences->keyBy(function ($item) { return Carbon::parse($item->date)->format('Y-m-d'); }); $currentDate = clone $start; while ($currentDate->lte($end)) { $formattedDate = $currentDate->format('d'); if ($presencesByDate->has($currentDate->format('Y-m-d'))) { $details[$user->id][$formattedDate] = $presencesByDate->get($currentDate->format('Y-m-d')); } else { $details[$user->id][$formattedDate] = null; } $currentDate->addDay(); } } $this->userAttendance = $details; return $details; } public function createPresence($userId,$day,$precence=null) : void { $user = User::find($userId); $objPrecence = null; if($precence)$objPrecence = Presence::find($precence); $fullDate = Carbon::parse($this->selectedDate)->setDay((int)$day)->format('Y-m-d'); if ($user) { $this->mountAction('createPresenceAction', ['user_id' => $userId,'date'=>$fullDate,'precence'=>$objPrecence]); } } public function saveAttendance():void { Notification::make() ->title('Save succefully') ->success() ->send(); } public function createPresenceAction(): Action { return Action::make('placeBidding') ->requiresConfirmation() ->modalIcon(fn(array $arguments) => isset($arguments['precence']) ?$arguments['precence']->description->getIcon(): 'heroicon-o-banknotes') ->modalHeading(fn(array $arguments):string => Carbon::parse($arguments['date'])->format('d.m.Y') ) ->modalDescription(fn(array $arguments) => isset($arguments['precence']) ?$arguments['precence']->description->getDescription(): 'Create new precence') ->modalSubmitActionLabel('Save') ->fillForm(function (array $arguments) { $precence = Presence::where('user_id', $arguments['user_id']) ->where('date', $arguments['date']) ->first(); if($precence){ return [ 'description' => $precence->description, 'entry_time' => $precence->entry_time, 'exit_time' => $precence->exit_time, ]; } }) ->form([ Select::make('description') ->options(Status::class) ->required(), TimePicker::make('entry_time'), TimePicker::make('exit_time') ]) ->action(function (array $data, array $arguments){ $user = User::find($arguments['user_id']); $date = ($arguments['date']); Presence::updateOrCreate( [ 'user_id' => $arguments['user_id'], 'date' => $date, ], [ 'description' => $data['description'], 'entry_time' => $data['entry_time'], 'exit_time' => $data['exit_time'], ] ); Notification::make() ->title('Attendance Successfully') ->success() ->body('Your bid has successfully') ->send(); }); } } |
В конце компонента мы добавили метод createPresenceAction
. Этот метод выполняет несколько важных функций:
- Он создает посещение сотрудника на конкретный день.
- Он создает модальное окно с настраиваемым значком, заголовком и описанием.
- Он определяет форму в модальном окне, включая поле ввода времени входа и выхода сотрудника и статус его явки на работу.
- Он загружает данные о посещении на указанный день по сотруднику и позволяем их поменять.
- Метод определяет действие, которое должно быть выполнено при отправке формы, включая создание или обновление записи о посещении и отправку уведомления об успешном завершении.
Для вызова этого действия был добавлен метод createPresence
Этот метод:
- Находит пользователя по идентификатору.
- Если мы передали посещение, то находим его.
- Формируем выбранную дату в нужном формате.
- Если пользователь найден, выполняется действие
createPresence
с передачей идентификатора пользователя, полной датой и типом посещения в качестве аргумента.
1 2 3 4 5 6 7 8 9 10 | public function createPresence($userId,$day,$precence=null) : void { $user = User::find($userId); $objPrecence = null; if($precence)$objPrecence = Presence::find($precence); $fullDate = Carbon::parse($this->selectedDate)->setDay((int)$day)->format('Y-m-d'); if ($user) { $this->mountAction('createPresenceAction', ['user_id' => $userId,'date'=>$fullDate,'precence'=>$objPrecence]); } } |
Заключение
Используя Action API от Filament и реактивность Livewire, мы создали динамичную и удобную для пользователя систему учета времени. Такой подход позволяет взаимодействовать и проверять данные в режиме реального времени, улучшая общее впечатление пользователей.
Ключом к бесперебойной работе является передача идентификатора посещения из шаблона Blade в компонент Livewire, а затем использование этого идентификатора для запуска соответствующего действия с необходимым контекстом. Этот шаблон можно адаптировать для различных других сценариев, в которых необходимо запускать сложные действия на основе взаимодействия пользователя с конкретными элементами в списке или таблице.