first&last

This commit is contained in:
sbb45
2026-03-20 15:47:17 +05:00
commit d6441abdd2
230 changed files with 20367 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Livewire\Auth;
use App\Http\Requests\Auth\LoginRequest;
use App\Services\ActivityLogService;
use App\Support\RoleRedirect;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.guest')]
class LoginPage extends Component
{
public string $email = '';
public string $password = '';
public bool $remember = false;
public bool $showPassword = false;
public function login(ActivityLogService $activityLogService)
{
$this->validate((new LoginRequest())->rules());
$key = strtolower($this->email).'|'.request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
throw ValidationException::withMessages([
'email' => 'Слишком много попыток входа. Попробуйте позже.',
]);
}
if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
RateLimiter::hit($key, 60);
throw ValidationException::withMessages([
'email' => 'Неверный email или пароль.',
]);
}
RateLimiter::clear($key);
request()->session()->regenerate();
$user = auth()->user();
$user->forceFill(['last_login_at' => now()])->save();
$activityLogService->log($user, $user, $user->id, 'login', 'Вход в систему', request()->ip());
return redirect()->intended(route(RoleRedirect::routeFor($user)));
}
public function render()
{
return view('livewire.auth.login-page')
->title('Вход в систему');
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace App\Livewire\Dashboard;
use App\Models\ActivityLog;
use App\Models\ServiceRequest;
use App\Models\Status;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class DashboardPage extends Component
{
public int $openToday = 0;
public int $overdue = 0;
public float $avgResolutionHours = 0;
public array $statusChart = [];
public array $priorityChart = [];
public array $topTechnicians = [];
public array $recentActivities = [];
public array $upcomingTasks = [];
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
$this->refreshData();
}
public function refreshData(): void
{
$this->openToday = ServiceRequest::query()
->open()
->whereDate('created_at', now()->toDateString())
->count();
$this->overdue = ServiceRequest::query()->overdue()->count();
$this->avgResolutionHours = (float) (ServiceRequest::query()
->whereNotNull('completed_at')
->whereDate('completed_at', '>=', now()->subDays(30))
->selectRaw('AVG(TIMESTAMPDIFF(HOUR, created_at, completed_at)) as avg_hours')
->value('avg_hours') ?? 0);
$this->statusChart = Status::query()
->orderBy('sort_order')
->get()
->map(fn (Status $status) => [
'label' => $status->name,
'value' => ServiceRequest::query()->where('status_id', $status->id)->count(),
'color' => $status->color_hex,
])
->toArray();
$this->priorityChart = ServiceRequest::query()
->join('priorities', 'priorities.id', '=', 'requests.priority_id')
->selectRaw('priorities.name as label, priorities.color_hex as color, COUNT(requests.id) as value')
->groupBy('priorities.id', 'priorities.name', 'priorities.color_hex')
->orderByRaw('MIN(priorities.level)')
->get()
->map(fn ($row) => [
'label' => $row->label,
'value' => (int) $row->value,
'color' => $row->color,
])
->toArray();
$this->topTechnicians = User::query()
->where('role', 'technician')
->withCount([
'assignedRequests as closed_requests_count' => fn (Builder $query) => $query->whereHas('status', fn (Builder $status) => $status->where('slug', 'closed')),
])
->orderByDesc('closed_requests_count')
->limit(5)
->get()
->map(fn (User $user) => [
'name' => $user->name,
'closed_requests_count' => (int) $user->closed_requests_count,
])
->toArray();
$this->recentActivities = ActivityLog::query()
->with('user')
->latest('created_at')
->limit(10)
->get()
->map(fn (ActivityLog $log) => [
'user' => $log->user?->name ?? 'Система',
'action' => $log->action,
'description' => $log->description,
'created_at' => $log->created_at?->format('d.m.Y H:i'),
])
->toArray();
$this->upcomingTasks = ServiceRequest::query()
->with(['assignee', 'status'])
->whereNotNull('planned_at')
->whereBetween('planned_at', [now(), now()->addDays(7)])
->orderBy('planned_at')
->limit(8)
->get()
->map(fn (ServiceRequest $request) => [
'ticket' => $request->ticket_number,
'title' => $request->title,
'planned_at' => $request->planned_at?->format('d.m.Y H:i'),
'assignee' => $request->assignee?->name ?? 'Не назначен',
])
->toArray();
$this->dispatch('dashboard-charts-updated', status: $this->statusChart, priority: $this->priorityChart);
}
public function render()
{
return view('livewire.dashboard.dashboard-page')->title('Дашборд');
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Livewire\Directories;
use App\Models\Department;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class DepartmentsPage extends Component
{
public string $name = '';
public string $description = '';
public ?int $editingId = null;
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function edit(int $id): void
{
$model = Department::query()->findOrFail($id);
$this->editingId = $model->id;
$this->name = $model->name;
$this->description = (string) $model->description;
}
public function save(): void
{
$nameRule = Rule::unique('departments', 'name');
if ($this->editingId) {
$nameRule = $nameRule->ignore($this->editingId);
}
$this->validate([
'name' => ['required', 'string', 'max:255', $nameRule],
'description' => ['nullable', 'string'],
]);
Department::query()->updateOrCreate(
['id' => $this->editingId],
['name' => $this->name, 'description' => $this->description]
);
$this->reset(['name', 'description', 'editingId']);
session()->flash('success', 'Отдел сохранен.');
}
public function delete(int $id): void
{
Department::query()->findOrFail($id)->delete();
session()->flash('success', 'Отдел удален.');
}
public function render()
{
return view('livewire.directories.departments-page', [
'items' => Department::query()->orderBy('name')->get(),
])->title('Справочник: Отделы');
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Livewire\Directories;
use App\Models\EquipmentCategory;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class EquipmentCategoriesPage extends Component
{
public string $name = '';
public string $description = '';
public ?int $editingId = null;
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function edit(int $id): void
{
$model = EquipmentCategory::query()->findOrFail($id);
$this->editingId = $model->id;
$this->name = $model->name;
$this->description = (string) $model->description;
}
public function save(): void
{
$this->validate([
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
]);
$slug = $this->generateUniqueSlug($this->name);
EquipmentCategory::query()->updateOrCreate(
['id' => $this->editingId],
['name' => $this->name, 'slug' => $slug, 'description' => $this->description]
);
$this->reset(['name', 'description', 'editingId']);
session()->flash('success', 'Категория сохранена.');
}
public function delete(int $id): void
{
EquipmentCategory::query()->findOrFail($id)->delete();
session()->flash('success', 'Категория удалена.');
}
private function generateUniqueSlug(string $name): string
{
$baseSlug = Str::slug($name);
if ($baseSlug === '') {
$baseSlug = 'category';
}
$slug = $baseSlug;
$suffix = 2;
while (
EquipmentCategory::query()
->where('slug', $slug)
->when($this->editingId, fn ($query) => $query->where('id', '!=', $this->editingId))
->exists()
) {
$slug = $baseSlug.'-'.$suffix;
$suffix++;
}
return $slug;
}
public function render()
{
return view('livewire.directories.equipment-categories-page', [
'items' => EquipmentCategory::query()->orderBy('name')->get(),
])->title('Справочник: Категории оборудования');
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace App\Livewire\Directories;
use App\Models\Equipment;
use App\Models\EquipmentCategory;
use App\Models\User;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Validation\Rule;
#[Layout('layouts.app')]
class EquipmentPage extends Component
{
use WithPagination;
public string $name = '';
public string $serialNumber = '';
public string $inventoryNumber = '';
public string $categoryId = '';
public string $clientId = '';
public string $purchaseDate = '';
public string $warrantyUntil = '';
public string $condition = 'good';
public string $notes = '';
public ?int $editingId = null;
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function edit(int $id): void
{
$model = Equipment::query()->findOrFail($id);
$this->editingId = $model->id;
$this->name = $model->name;
$this->serialNumber = $model->serial_number;
$this->inventoryNumber = $model->inventory_number;
$this->categoryId = (string) $model->category_id;
$this->clientId = (string) $model->client_id;
$this->purchaseDate = (string) $model->purchase_date;
$this->warrantyUntil = (string) $model->warranty_until;
$this->condition = $model->condition->value;
$this->notes = (string) $model->notes;
}
public function save(): void
{
$serialRule = Rule::unique('equipment', 'serial_number');
$inventoryRule = Rule::unique('equipment', 'inventory_number');
if ($this->editingId) {
$serialRule = $serialRule->ignore($this->editingId);
$inventoryRule = $inventoryRule->ignore($this->editingId);
}
$this->validate([
'name' => ['required', 'string', 'max:255'],
'serialNumber' => ['required', 'string', $serialRule],
'inventoryNumber' => ['required', 'string', $inventoryRule],
'categoryId' => ['required', 'exists:equipment_categories,id'],
'clientId' => ['nullable', 'exists:users,id'],
'purchaseDate' => ['nullable', 'date'],
'warrantyUntil' => ['nullable', 'date'],
'condition' => ['required', Rule::in(['good', 'fair', 'poor'])],
'notes' => ['nullable', 'string'],
]);
Equipment::query()->updateOrCreate(
['id' => $this->editingId],
[
'name' => $this->name,
'serial_number' => $this->serialNumber,
'inventory_number' => $this->inventoryNumber,
'category_id' => $this->categoryId,
'client_id' => $this->clientId ?: null,
'purchase_date' => $this->purchaseDate ?: null,
'warranty_until' => $this->warrantyUntil ?: null,
'condition' => $this->condition,
'notes' => $this->notes,
]
);
$this->reset(['name', 'serialNumber', 'inventoryNumber', 'categoryId', 'clientId', 'purchaseDate', 'warrantyUntil', 'notes', 'editingId']);
$this->condition = 'good';
session()->flash('success', 'Оборудование сохранено.');
}
public function delete(int $id): void
{
Equipment::query()->findOrFail($id)->delete();
session()->flash('success', 'Оборудование удалено.');
}
public function render()
{
return view('livewire.directories.equipment-page', [
'items' => Equipment::query()->with(['category', 'client'])->latest()->paginate(15),
'categories' => EquipmentCategory::query()->orderBy('name')->get(),
'clients' => User::query()->where('role', 'client')->orderBy('name')->get(),
])->title('Справочник: Оборудование');
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Livewire\Directories;
use App\Models\Priority;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class PrioritiesPage extends Component
{
public string $name = '';
public string $colorHex = '#3B82F6';
public int $level = 1;
public ?int $editingId = null;
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function edit(int $id): void
{
$model = Priority::query()->findOrFail($id);
$this->editingId = $model->id;
$this->name = $model->name;
$this->colorHex = $model->color_hex;
$this->level = $model->level;
}
public function save(): void
{
$levelRule = Rule::unique('priorities', 'level');
if ($this->editingId) {
$levelRule = $levelRule->ignore($this->editingId);
}
$this->validate([
'name' => ['required', 'string', 'max:255'],
'colorHex' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'level' => ['required', 'integer', 'between:1,4', $levelRule],
]);
Priority::query()->updateOrCreate(
['id' => $this->editingId],
['name' => $this->name, 'color_hex' => $this->colorHex, 'level' => $this->level]
);
$this->reset(['name', 'editingId']);
$this->colorHex = '#3B82F6';
$this->level = 1;
session()->flash('success', 'Приоритет сохранен.');
}
public function delete(int $id): void
{
Priority::query()->findOrFail($id)->delete();
session()->flash('success', 'Приоритет удален.');
}
public function render()
{
return view('livewire.directories.priorities-page', [
'items' => Priority::query()->orderBy('level')->get(),
])->title('Справочник: Приоритеты');
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Livewire\Directories;
use App\Models\RequestType;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class RequestTypesPage extends Component
{
public string $name = '';
public string $description = '';
public int $slaHours = 24;
public ?int $editingId = null;
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function edit(int $id): void
{
$model = RequestType::query()->findOrFail($id);
$this->editingId = $model->id;
$this->name = $model->name;
$this->description = (string) $model->description;
$this->slaHours = $model->sla_hours;
}
public function save(): void
{
$nameRule = Rule::unique('request_types', 'name');
if ($this->editingId) {
$nameRule = $nameRule->ignore($this->editingId);
}
$this->validate([
'name' => ['required', 'string', 'max:255', $nameRule],
'description' => ['nullable', 'string'],
'slaHours' => ['required', 'integer', 'min:1', 'max:720'],
]);
RequestType::query()->updateOrCreate(
['id' => $this->editingId],
['name' => $this->name, 'description' => $this->description, 'sla_hours' => $this->slaHours]
);
$this->reset(['name', 'description', 'editingId']);
$this->slaHours = 24;
session()->flash('success', 'Тип заявки сохранен.');
}
public function delete(int $id): void
{
RequestType::query()->findOrFail($id)->delete();
session()->flash('success', 'Тип заявки удален.');
}
public function render()
{
return view('livewire.directories.request-types-page', [
'items' => RequestType::query()->orderBy('name')->get(),
])->title('Справочник: Типы заявок');
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace App\Livewire\Directories;
use App\Models\Status;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class StatusesPage extends Component
{
public string $name = '';
public string $colorHex = '#3B82F6';
public string $slug = '';
public bool $isFinal = false;
public int $sortOrder = 1;
public ?int $editingId = null;
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function edit(int $id): void
{
$model = Status::query()->findOrFail($id);
$this->editingId = $model->id;
$this->name = $model->name;
$this->colorHex = $model->color_hex;
$this->slug = $model->slug;
$this->isFinal = $model->is_final;
$this->sortOrder = $model->sort_order;
}
public function save(): void
{
$slugRule = Rule::unique('statuses', 'slug');
if ($this->editingId) {
$slugRule = $slugRule->ignore($this->editingId);
}
$this->validate([
'name' => ['required', 'string', 'max:255'],
'colorHex' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'slug' => ['required', 'string', $slugRule],
'isFinal' => ['boolean'],
'sortOrder' => ['required', 'integer', 'min:1', 'max:100'],
]);
Status::query()->updateOrCreate(
['id' => $this->editingId],
[
'name' => $this->name,
'color_hex' => $this->colorHex,
'slug' => $this->slug,
'is_final' => $this->isFinal,
'sort_order' => $this->sortOrder,
]
);
$this->reset(['name', 'slug', 'editingId']);
$this->colorHex = '#3B82F6';
$this->isFinal = false;
$this->sortOrder = 1;
session()->flash('success', 'Статус сохранен.');
}
public function delete(int $id): void
{
$status = Status::query()->findOrFail($id);
if ($status->requests()->exists()) {
session()->flash('error', 'Нельзя удалить статус, который используется в заявках.');
return;
}
$status->delete();
session()->flash('success', 'Статус удален.');
}
public function render()
{
return view('livewire.directories.statuses-page', [
'items' => Status::query()->orderBy('sort_order')->get(),
])->title('Справочник: Статусы');
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Livewire\MyRequests;
use App\Enums\StatusSlug;
use App\Models\Equipment;
use App\Models\Priority;
use App\Models\RequestType;
use App\Models\Status;
use App\Services\RequestService;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class MyRequestFormPage extends Component
{
public string $title = '';
public string $description = '';
public string $typeId = '';
public string $equipmentId = '';
public string $priorityId = '';
public function mount(): void
{
abort_unless(auth()->user()->isClient(), 403);
$this->priorityId = (string) Priority::query()->where('level', 2)->value('id');
}
public function save(RequestService $requestService)
{
$this->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['required', 'string'],
'typeId' => ['required', 'exists:request_types,id'],
'priorityId' => ['required', 'exists:priorities,id'],
'equipmentId' => ['nullable', 'exists:equipment,id'],
]);
$statusId = (string) Status::query()->where('slug', StatusSlug::NEW->value)->value('id');
$request = $requestService->create([
'title' => $this->title,
'description' => $this->description,
'type_id' => $this->typeId,
'priority_id' => $this->priorityId,
'status_id' => $statusId,
'client_id' => auth()->id(),
'equipment_id' => $this->equipmentId ?: null,
'assigned_to' => null,
], auth()->user());
session()->flash('success', 'Заявка создана.');
return redirect()->route('my-requests.show', $request);
}
public function render()
{
return view('livewire.my-requests.my-request-form-page', [
'types' => RequestType::query()->orderBy('name')->get(),
'equipmentItems' => Equipment::query()->where('client_id', auth()->id())->orderBy('name')->get(),
'priorities' => Priority::query()->orderBy('level')->get(),
])->title('Создание заявки');
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Livewire\MyRequests;
use App\Models\ServiceRequest;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
class MyRequestsIndexPage extends Component
{
use WithPagination;
public string $search = '';
public function render()
{
$requests = ServiceRequest::query()
->with(['status', 'priority', 'type'])
->where('client_id', auth()->id())
->when($this->search !== '', function ($query): void {
$query->where(function ($q): void {
$q->where('ticket_number', 'like', "%{$this->search}%")
->orWhere('title', 'like', "%{$this->search}%");
});
})
->latest()
->paginate(15);
return view('livewire.my-requests.my-requests-index-page', [
'requests' => $requests,
])->title('Мои заявки');
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Livewire\Notifications;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
class NotificationsPage extends Component
{
use WithPagination;
public function markAllAsRead(): void
{
auth()->user()?->unreadNotifications->markAsRead();
session()->flash('success', 'Все уведомления помечены как прочитанные.');
}
public function markAsRead(string $id): void
{
$notification = auth()->user()?->notifications()->findOrFail($id);
$notification->markAsRead();
}
public function render()
{
$notifications = auth()->user()
?->notifications()
->latest()
->paginate(15);
return view('livewire.notifications.notifications-page', [
'notifications' => $notifications,
])->title('Уведомления');
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Livewire\Profile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class ProfilePage extends Component
{
public string $name = '';
public string $email = '';
public string $phone = '';
public string $password = '';
public string $password_confirmation = '';
public function mount(): void
{
$user = auth()->user();
$this->name = $user->name;
$this->email = $user->email;
$this->phone = (string) $user->phone;
}
public function save(): void
{
$this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore(auth()->id())],
'phone' => ['nullable', 'string', 'max:32'],
'password' => ['nullable', 'string', 'same:password_confirmation', 'min:8'],
]);
$user = auth()->user();
$payload = [
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
];
if ($this->password !== '') {
$payload['password'] = Hash::make($this->password);
}
$user->update($payload);
session()->flash('success', 'Профиль обновлен.');
}
public function render()
{
return view('livewire.profile.profile-page')->title('Профиль');
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Livewire\Reports;
use App\Services\ReportService;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class EquipmentReportPage extends Component
{
public string $dateFrom = '';
public string $dateTo = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
}
public function render(ReportService $reportService)
{
$rows = $reportService->equipmentFailureFrequency([
'date_from' => $this->dateFrom ?: null,
'date_to' => $this->dateTo ?: null,
]);
return view('livewire.reports.equipment-report-page', [
'rows' => $rows,
])->title('Отчет: Оборудование');
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Livewire\Reports;
use App\Models\Status;
use App\Models\RequestType;
use App\Models\User;
use App\Services\ReportService;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class GeneralReportPage extends Component
{
public string $dateFrom = '';
public string $dateTo = '';
public string $technicianId = '';
public string $typeId = '';
public string $statusId = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
}
public function render(ReportService $reportService)
{
$filters = [
'date_from' => $this->dateFrom ?: null,
'date_to' => $this->dateTo ?: null,
'technician_id' => $this->technicianId ?: null,
'type_id' => $this->typeId ?: null,
'status_id' => $this->statusId ?: null,
];
$rows = $reportService->requestBaseQuery($filters)->latest()->limit(200)->get();
$statusChart = $reportService->requestsByStatus($filters);
$this->dispatch('general-report-chart-updated', chart: $statusChart->values()->toArray());
return view('livewire.reports.general-report-page', [
'rows' => $rows,
'statusChart' => $statusChart,
'technicians' => User::query()->where('role', 'technician')->orderBy('name')->get(),
'types' => RequestType::query()->orderBy('name')->get(),
'statuses' => Status::query()->orderBy('sort_order')->get(),
])->title('Отчет: Общий');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Livewire\Reports;
use App\Services\ReportService;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class SlaReportPage extends Component
{
public string $dateFrom = '';
public string $dateTo = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
}
public function render(ReportService $reportService)
{
$rows = $reportService->slaCompliance([
'date_from' => $this->dateFrom ?: null,
'date_to' => $this->dateTo ?: null,
]);
return view('livewire.reports.sla-report-page', [
'rows' => $rows,
])->title('Отчет: SLA');
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Livewire\Reports;
use App\Exports\CsvExport;
use App\Services\ReportService;
use Barryvdh\DomPDF\Facade\Pdf;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class TechniciansReportPage extends Component
{
public string $dateFrom = '';
public string $dateTo = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
}
public function exportCsv(ReportService $reportService)
{
$rows = $reportService->technicianPerformance([
'date_from' => $this->dateFrom ?: null,
'date_to' => $this->dateTo ?: null,
])->values()->all();
return CsvExport::download('technicians-report.csv', [
'Техник', 'Назначено', 'Решено', 'Среднее время (ч)', 'Рейтинг', 'Просрочено',
], array_map(fn ($item) => [
$item['technician'],
$item['assigned'],
$item['resolved'],
$item['avg_resolution_hours'],
$item['rating'],
$item['overdue'],
], $rows));
}
public function exportPdf(ReportService $reportService)
{
$rows = $reportService->technicianPerformance([
'date_from' => $this->dateFrom ?: null,
'date_to' => $this->dateTo ?: null,
])->values()->all();
$pdf = Pdf::loadView('exports.technicians-report-pdf', [
'rows' => $rows,
'generatedAt' => now()->format('d.m.Y H:i'),
]);
return response()->streamDownload(fn () => print($pdf->output()), 'technicians-report.pdf');
}
public function render(ReportService $reportService)
{
$rows = $reportService->technicianPerformance([
'date_from' => $this->dateFrom ?: null,
'date_to' => $this->dateTo ?: null,
]);
return view('livewire.reports.technicians-report-page', [
'rows' => $rows,
])->title('Отчет: Эффективность техников');
}
}
+172
View File
@@ -0,0 +1,172 @@
<?php
namespace App\Livewire\Requests;
use App\Enums\StatusSlug;
use App\Models\Equipment;
use App\Models\Priority;
use App\Models\RequestType;
use App\Models\ServiceRequest;
use App\Models\Status;
use App\Models\User;
use App\Services\RequestService;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\WithFileUploads;
#[Layout('layouts.app')]
class RequestFormPage extends Component
{
use WithFileUploads;
public ?ServiceRequest $serviceRequest = null;
public ?int $serviceRequestId = null;
public string $title = '';
public string $description = '';
public string $typeId = '';
public string $priorityId = '';
public string $statusId = '';
public string $clientId = '';
public string $assignedTo = '';
public string $equipmentId = '';
public string $location = '';
public string $plannedAt = '';
public string $deadlineAt = '';
public string $resolutionNotes = '';
public ?int $clientRating = null;
public string $clientComment = '';
public array $attachments = [];
public function mount(?ServiceRequest $request = null): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
if ($request && $request->exists) {
$this->serviceRequest = $request;
$this->serviceRequestId = $request->id;
$this->title = $request->title;
$this->description = $request->description;
$this->typeId = (string) $request->type_id;
$this->priorityId = (string) $request->priority_id;
$this->statusId = (string) $request->status_id;
$this->clientId = (string) $request->client_id;
$this->assignedTo = (string) $request->assigned_to;
$this->equipmentId = (string) $request->equipment_id;
$this->location = (string) $request->location;
$this->plannedAt = (string) $request->planned_at?->format('Y-m-d\TH:i');
$this->deadlineAt = (string) $request->deadline_at?->format('Y-m-d\TH:i');
$this->resolutionNotes = (string) $request->resolution_notes;
$this->clientRating = $request->client_rating;
$this->clientComment = (string) $request->client_comment;
} else {
$this->statusId = (string) Status::query()->where('slug', StatusSlug::NEW->value)->value('id');
}
}
public function updatedTypeId(): void
{
if (!$this->typeId) {
return;
}
$type = RequestType::query()->find($this->typeId);
if ($type) {
$base = $this->plannedAt !== '' ? Carbon::parse($this->plannedAt) : now();
$this->deadlineAt = $base->addHours($type->sla_hours)->format('Y-m-d\TH:i');
}
}
public function save(RequestService $requestService)
{
$this->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['required', 'string'],
'typeId' => ['required', 'exists:request_types,id'],
'priorityId' => ['required', 'exists:priorities,id'],
'statusId' => ['nullable', 'exists:statuses,id'],
'clientId' => ['required', 'exists:users,id'],
'assignedTo' => ['nullable', 'exists:users,id'],
'equipmentId' => ['nullable', 'exists:equipment,id'],
'location' => ['nullable', 'string', 'max:255'],
'plannedAt' => ['nullable', 'date'],
'deadlineAt' => ['nullable', 'date'],
'resolutionNotes' => ['nullable', 'string'],
'clientRating' => ['nullable', 'integer', 'between:1,5'],
'clientComment' => ['nullable', 'string', 'max:2000'],
'attachments' => ['nullable', 'array', 'max:5'],
'attachments.*' => ['file', 'max:10240'],
]);
if (!$this->serviceRequestId && empty($this->statusId)) {
throw ValidationException::withMessages([
'statusId' => 'Не удалось определить статус заявки.',
]);
}
$payload = [
'title' => $this->title,
'description' => $this->description,
'type_id' => $this->typeId,
'priority_id' => $this->priorityId,
'status_id' => $this->statusId,
'client_id' => $this->clientId,
'assigned_to' => $this->assignedTo ?: null,
'equipment_id' => $this->equipmentId ?: null,
'location' => $this->location,
'planned_at' => $this->plannedAt ?: null,
'deadline_at' => $this->deadlineAt ?: null,
'resolution_notes' => $this->resolutionNotes ?: null,
'client_rating' => $this->clientRating,
'client_comment' => $this->clientComment ?: null,
'attachments' => $this->attachments,
];
if ($this->serviceRequestId && $this->serviceRequest) {
$request = $requestService->update($this->serviceRequest, $payload, auth()->user());
session()->flash('success', 'Заявка успешно обновлена.');
return redirect()->route('requests.show', $request);
}
$request = $requestService->create($payload, auth()->user());
session()->flash('success', 'Заявка успешно создана.');
return redirect()->route('requests.show', $request);
}
public function render()
{
$clientId = $this->clientId !== '' ? (int) $this->clientId : null;
return view('livewire.requests.request-form-page', [
'types' => RequestType::query()->orderBy('name')->get(),
'priorities' => Priority::query()->orderBy('level')->get(),
'statuses' => Status::query()->orderBy('sort_order')->get(),
'clients' => User::query()->where('role', 'client')->orderBy('name')->get(),
'technicians' => User::query()->where('role', 'technician')->orderBy('name')->get(),
'equipmentItems' => Equipment::query()
->when($clientId, fn ($q) => $q->where('client_id', $clientId))
->orderBy('name')
->get(),
])->title($this->serviceRequestId ? 'Редактирование заявки' : 'Создание заявки');
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace App\Livewire\Requests;
use App\Exports\CsvExport;
use App\Models\Priority;
use App\Models\RequestType;
use App\Models\ServiceRequest;
use App\Models\Status;
use App\Models\User;
use App\Services\ActivityLogService;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
class RequestIndexPage extends Component
{
use WithPagination;
public string $search = '';
public string $statusId = '';
public string $priorityId = '';
public string $typeId = '';
public string $technicianId = '';
public string $dateFrom = '';
public string $dateTo = '';
public string $sortField = 'created_at';
public string $sortDirection = 'desc';
public int $perPage = 15;
public array $selected = [];
public string $bulkAssignee = '';
public string $bulkStatus = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
}
public function updatingSearch(): void
{
$this->resetPage();
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
return;
}
$this->sortField = $field;
$this->sortDirection = 'asc';
}
public function clearFilters(): void
{
$this->reset(['search', 'statusId', 'priorityId', 'typeId', 'technicianId', 'dateFrom', 'dateTo']);
$this->resetPage();
}
public function bulkAssign(ActivityLogService $activityLogService): void
{
if (!$this->bulkAssignee || empty($this->selected)) {
return;
}
ServiceRequest::query()
->whereIn('id', $this->selected)
->update(['assigned_to' => $this->bulkAssignee]);
$activityLogService->log(auth()->user(), ServiceRequest::class, 0, 'bulk_assign', 'Массовое назначение заявок', request()->ip());
$this->selected = [];
session()->flash('success', 'Исполнитель назначен выбранным заявкам.');
}
public function bulkChangeStatus(ActivityLogService $activityLogService): void
{
if (!$this->bulkStatus || empty($this->selected)) {
return;
}
ServiceRequest::query()
->whereIn('id', $this->selected)
->update(['status_id' => $this->bulkStatus]);
$activityLogService->log(auth()->user(), ServiceRequest::class, 0, 'bulk_status_change', 'Массовая смена статуса заявок', request()->ip());
$this->selected = [];
session()->flash('success', 'Статус обновлен для выбранных заявок.');
}
public function exportCsv()
{
$rows = $this->query()->get()->map(function (ServiceRequest $request) {
return [
$request->ticket_number,
$request->title,
$request->client?->name,
$request->assignee?->name,
$request->priority?->name,
$request->status?->name,
$request->deadline_at?->format('d.m.Y H:i'),
$request->created_at?->format('d.m.Y H:i'),
];
})->toArray();
return CsvExport::download(
'requests.csv',
['Ticket', 'Заголовок', 'Клиент', 'Исполнитель', 'Приоритет', 'Статус', 'Дедлайн', 'Создана'],
$rows
);
}
private function query(): Builder
{
return ServiceRequest::query()
->with(['client', 'assignee', 'priority', 'status', 'type'])
->when($this->search !== '', function (Builder $query): void {
$query->where(function (Builder $q): void {
$q->where('title', 'like', "%{$this->search}%")
->orWhere('ticket_number', 'like', "%{$this->search}%");
});
})
->when($this->statusId !== '', fn (Builder $q) => $q->where('status_id', $this->statusId))
->when($this->priorityId !== '', fn (Builder $q) => $q->where('priority_id', $this->priorityId))
->when($this->typeId !== '', fn (Builder $q) => $q->where('type_id', $this->typeId))
->when($this->technicianId !== '', fn (Builder $q) => $q->where('assigned_to', $this->technicianId))
->when($this->dateFrom !== '', fn (Builder $q) => $q->whereDate('created_at', '>=', $this->dateFrom))
->when($this->dateTo !== '', fn (Builder $q) => $q->whereDate('created_at', '<=', $this->dateTo))
->orderBy($this->sortField, $this->sortDirection);
}
public function render()
{
return view('livewire.requests.request-index-page', [
'requests' => $this->query()->paginate($this->perPage),
'statuses' => Status::query()->orderBy('sort_order')->get(),
'priorities' => Priority::query()->orderBy('level')->get(),
'types' => RequestType::query()->orderBy('name')->get(),
'technicians' => User::query()->where('role', 'technician')->orderBy('name')->get(),
])->title('Заявки');
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace App\Livewire\Requests;
use App\Models\RequestComment;
use App\Models\ServiceRequest;
use App\Models\Status;
use App\Services\NotificationService;
use App\Services\RequestService;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class RequestShowPage extends Component
{
public ServiceRequest $request;
public string $commentBody = '';
public bool $commentInternal = false;
public string $nextStatusId = '';
public ?int $rating = null;
public string $ratingComment = '';
public function mount(ServiceRequest $request): void
{
Gate::authorize('view', $request);
$this->request = $request->load([
'type',
'priority',
'status',
'client',
'assignee',
'equipment.category',
'comments.user',
'attachments.user',
]);
}
public function addComment(NotificationService $notificationService): void
{
Gate::authorize('update', $this->request);
$data = $this->validate([
'commentBody' => ['required', 'string', 'max:5000'],
'commentInternal' => ['nullable', 'boolean'],
]);
if (($data['commentInternal'] ?? false) && !auth()->user()->isStaff()) {
abort(403);
}
$comment = $this->request->comments()->create([
'user_id' => auth()->id(),
'body' => $data['commentBody'],
'is_internal' => (bool) ($data['commentInternal'] ?? false),
]);
$this->commentBody = '';
$this->commentInternal = false;
$notificationService->notifyCommentAdded($this->request->fresh(['assignee', 'client']), auth()->user());
session()->flash('success', 'Комментарий добавлен.');
$this->request->refresh();
}
public function deleteComment(int $commentId): void
{
$comment = RequestComment::query()->findOrFail($commentId);
abort_unless($comment->user_id === auth()->id(), 403);
$comment->delete();
session()->flash('success', 'Комментарий удален.');
$this->request->refresh();
}
public function changeStatus(RequestService $requestService): void
{
Gate::authorize('update', $this->request);
if ($this->nextStatusId === '') {
return;
}
$target = Status::query()->findOrFail($this->nextStatusId);
$allowed = $requestService->allowedTransitions($this->request->fresh('status'), auth()->user());
if (!in_array($target->slug, $allowed, true)) {
abort(422, 'Переход статуса недоступен.');
}
$requestService->changeStatus($this->request, $target, auth()->user());
$this->nextStatusId = '';
session()->flash('success', 'Статус обновлен.');
$this->request->refresh();
}
public function submitRating(): void
{
Gate::authorize('rate', $this->request);
$data = $this->validate([
'rating' => ['required', 'integer', 'between:1,5'],
'ratingComment' => ['nullable', 'string', 'max:2000'],
]);
$this->request->update([
'client_rating' => $data['rating'],
'client_comment' => $data['ratingComment'] ?? null,
]);
session()->flash('success', 'Спасибо за оценку заявки.');
$this->request->refresh();
}
private function allowedStatuses()
{
$requestService = app(RequestService::class);
$slugs = $requestService->allowedTransitions($this->request->fresh('status'), auth()->user());
return Status::query()->whereIn('slug', $slugs)->orderBy('sort_order')->get();
}
public function render()
{
$isClient = auth()->user()->isClient();
$comments = $this->request->comments()
->with('user')
->when($isClient, fn ($q) => $q->where('is_internal', false))
->latest()
->get();
return view('livewire.requests.request-show-page', [
'allowedStatuses' => $this->allowedStatuses(),
'comments' => $comments,
'activity' => $this->request->id
? \App\Models\ActivityLog::query()
->where('model_type', ServiceRequest::class)
->where('model_id', $this->request->id)
->with('user')
->latest('created_at')
->limit(20)
->get()
: collect(),
])->title("Заявка {$this->request->ticket_number}");
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Livewire\System;
use App\Exports\CsvExport;
use App\Models\ActivityLog;
use App\Models\User;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
class ActivityLogPage extends Component
{
use WithPagination;
public string $userId = '';
public string $action = '';
public string $dateFrom = '';
public string $dateTo = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin(), 403);
}
public function exportCsv()
{
$rows = $this->query()->get()->map(fn (ActivityLog $log) => [
$log->user?->name,
$log->action,
class_basename($log->model_type),
$log->description,
$log->ip_address,
$log->created_at?->format('d.m.Y H:i:s'),
])->toArray();
return CsvExport::download('activity-log.csv', ['Пользователь', 'Действие', 'Сущность', 'Описание', 'IP', 'Дата'], $rows);
}
private function query()
{
return ActivityLog::query()
->with('user')
->when($this->userId !== '', fn ($q) => $q->where('user_id', $this->userId))
->when($this->action !== '', fn ($q) => $q->where('action', 'like', "%{$this->action}%"))
->when($this->dateFrom !== '', fn ($q) => $q->whereDate('created_at', '>=', $this->dateFrom))
->when($this->dateTo !== '', fn ($q) => $q->whereDate('created_at', '<=', $this->dateTo))
->latest('created_at');
}
public function render()
{
return view('livewire.system.activity-log-page', [
'logs' => $this->query()->paginate(20),
'users' => User::query()->orderBy('name')->get(),
])->title('Журнал действий');
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace App\Livewire\System;
use App\Services\SettingsService;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class SettingsPage extends Component
{
public string $companyName = '';
public string $timezone = 'Asia/Yekaterinburg';
public int $defaultSlaHours = 24;
public bool $maintenanceMode = false;
public bool $notifyAssigned = true;
public bool $notifyStatusChanged = true;
public bool $notifyCommentAdded = true;
public bool $notifyOverdue = true;
public bool $notifySlaWarning = true;
/**
* @var array<string, string>
*/
public array $russianTimezones = [
'Europe/Kaliningrad' => 'Калининград (UTC+2)',
'Europe/Moscow' => 'Москва (UTC+3)',
'Europe/Samara' => 'Самара (UTC+4)',
'Asia/Yekaterinburg' => 'Екатеринбург (UTC+5)',
'Asia/Omsk' => 'Омск (UTC+6)',
'Asia/Krasnoyarsk' => 'Красноярск (UTC+7)',
'Asia/Irkutsk' => 'Иркутск (UTC+8)',
'Asia/Yakutsk' => 'Якутск (UTC+9)',
'Asia/Vladivostok' => 'Владивосток (UTC+10)',
'Asia/Magadan' => 'Магадан (UTC+11)',
'Asia/Sakhalin' => 'Сахалин (UTC+11)',
'Asia/Kamchatka' => 'Камчатка (UTC+12)',
];
public function mount(SettingsService $settingsService): void
{
abort_unless(auth()->user()->isAdmin(), 403);
$this->companyName = (string) $settingsService->get('company_name', 'Computer Service');
$this->timezone = (string) $settingsService->get('timezone', 'Asia/Yekaterinburg');
$this->defaultSlaHours = (int) $settingsService->get('default_sla_hours', 24);
$this->maintenanceMode = (bool) $settingsService->get('maintenance_mode', false);
$notifications = $settingsService->emailNotifications();
$this->notifyAssigned = (bool) ($notifications['assigned'] ?? true);
$this->notifyStatusChanged = (bool) ($notifications['status_changed'] ?? true);
$this->notifyCommentAdded = (bool) ($notifications['comment_added'] ?? true);
$this->notifyOverdue = (bool) ($notifications['overdue'] ?? true);
$this->notifySlaWarning = (bool) ($notifications['sla_warning'] ?? true);
}
public function save(SettingsService $settingsService): void
{
$this->validate([
'companyName' => ['required', 'string', 'max:255'],
'timezone' => ['required', Rule::in(array_keys($this->russianTimezones))],
'defaultSlaHours' => ['required', 'integer', 'min:1', 'max:720'],
'maintenanceMode' => ['boolean'],
'notifyAssigned' => ['boolean'],
'notifyStatusChanged' => ['boolean'],
'notifyCommentAdded' => ['boolean'],
'notifyOverdue' => ['boolean'],
'notifySlaWarning' => ['boolean'],
]);
$settingsService->set('company_name', $this->companyName, 'string');
$settingsService->set('timezone', $this->timezone, 'string');
$settingsService->set('default_sla_hours', $this->defaultSlaHours, 'integer');
$settingsService->set('maintenance_mode', $this->maintenanceMode, 'boolean');
$settingsService->set('email_notifications', [
'assigned' => $this->notifyAssigned,
'status_changed' => $this->notifyStatusChanged,
'comment_added' => $this->notifyCommentAdded,
'overdue' => $this->notifyOverdue,
'sla_warning' => $this->notifySlaWarning,
], 'json');
session()->flash('success', 'Настройки сохранены.');
}
public function render()
{
return view('livewire.system.settings-page')->title('Системные настройки');
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Livewire\Tasks;
use App\Models\ServiceRequest;
use App\Models\Status;
use App\Services\RequestService;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class MyTasksPage extends Component
{
public string $priorityId = '';
public string $deadlineFilter = '';
public array $quickComments = [];
public function mount(): void
{
abort_unless(auth()->user()->isTechnician(), 403);
}
public function updateStatus(int $requestId, string $statusId, RequestService $requestService): void
{
$request = ServiceRequest::query()->with('status')->findOrFail($requestId);
abort_unless($request->assigned_to === auth()->id(), 403);
$target = Status::query()->findOrFail($statusId);
$allowed = $requestService->allowedTransitions($request, auth()->user());
if (!in_array($target->slug, $allowed, true)) {
return;
}
$requestService->changeStatus($request, $target, auth()->user());
session()->flash('success', "Статус {$request->ticket_number} обновлен.");
}
public function transitionOptions(ServiceRequest $request)
{
$slugs = app(RequestService::class)->allowedTransitions($request, auth()->user());
return Status::query()->whereIn('slug', $slugs)->orderBy('sort_order')->get();
}
public function addQuickComment(int $requestId): void
{
$body = trim((string) ($this->quickComments[$requestId] ?? ''));
if ($body === '') {
return;
}
$request = ServiceRequest::query()->findOrFail($requestId);
abort_unless($request->assigned_to === auth()->id(), 403);
$request->comments()->create([
'user_id' => auth()->id(),
'body' => $body,
'is_internal' => true,
]);
$this->quickComments[$requestId] = '';
session()->flash('success', "Комментарий к {$request->ticket_number} добавлен.");
}
public function render()
{
$statuses = Status::query()->orderBy('sort_order')->get();
$requests = ServiceRequest::query()
->with(['status', 'priority', 'client', 'equipment'])
->where('assigned_to', auth()->id())
->when($this->priorityId !== '', fn ($q) => $q->where('priority_id', $this->priorityId))
->when($this->deadlineFilter === 'today', fn ($q) => $q->whereDate('deadline_at', now()->toDateString()))
->when($this->deadlineFilter === 'overdue', fn ($q) => $q->overdue())
->orderBy('deadline_at')
->get()
->groupBy(fn (ServiceRequest $request) => $request->status?->slug ?? 'unknown');
return view('livewire.tasks.my-tasks-page', [
'statuses' => $statuses,
'requestsByStatus' => $requests,
'priorities' => \App\Models\Priority::query()->orderBy('level')->get(),
])->title('Мои задачи');
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Livewire\Users;
use App\Models\Department;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class UserFormPage extends Component
{
public ?User $userModel = null;
public string $name = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public string $role = 'client';
public string $departmentId = '';
public string $phone = '';
public bool $isActive = true;
public function mount(?User $user = null): void
{
abort_unless(auth()->user()->isAdmin(), 403);
if ($user && $user->exists) {
$this->userModel = $user;
$this->name = $user->name;
$this->email = $user->email;
$this->role = $user->role->value;
$this->departmentId = (string) $user->department_id;
$this->phone = (string) $user->phone;
$this->isActive = (bool) $user->is_active;
}
}
public function save()
{
$emailRule = Rule::unique('users', 'email');
if ($this->userModel) {
$emailRule = $emailRule->ignore($this->userModel->id);
}
$rules = [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', $emailRule],
'role' => ['required', Rule::in(['admin', 'manager', 'technician', 'client'])],
'departmentId' => ['nullable', 'exists:departments,id'],
'phone' => ['nullable', 'string', 'max:32'],
'isActive' => ['boolean'],
];
if ($this->userModel) {
$rules['password'] = ['nullable', 'string', 'min:8', 'same:password_confirmation'];
} else {
$rules['password'] = ['required', 'string', 'min:8', 'same:password_confirmation'];
}
$this->validate($rules);
$payload = [
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'department_id' => $this->departmentId ?: null,
'phone' => $this->phone,
'is_active' => $this->isActive,
];
if ($this->password !== '') {
$payload['password'] = Hash::make($this->password);
}
if ($this->userModel) {
$this->userModel->update($payload);
session()->flash('success', 'Пользователь обновлен.');
return redirect()->route('users.show', $this->userModel);
}
$payload['password'] = Hash::make($this->password);
$user = User::query()->create($payload);
session()->flash('success', 'Пользователь создан.');
return redirect()->route('users.show', $user);
}
public function render()
{
return view('livewire.users.user-form-page', [
'departments' => Department::query()->orderBy('name')->get(),
])->title($this->userModel ? 'Редактирование пользователя' : 'Создание пользователя');
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Livewire\Users;
use App\Models\ActivityLog;
use App\Models\ServiceRequest;
use App\Models\User;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class UserShowPage extends Component
{
public User $user;
public function mount(User $user): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
$this->user = $user->load('department');
}
public function render()
{
$assigned = ServiceRequest::query()->where('assigned_to', $this->user->id)->count();
$resolved = ServiceRequest::query()
->where('assigned_to', $this->user->id)
->whereHas('status', fn ($q) => $q->whereIn('slug', ['resolved', 'closed']))
->count();
$avgRating = ServiceRequest::query()->where('assigned_to', $this->user->id)->whereNotNull('client_rating')->avg('client_rating');
return view('livewire.users.user-show-page', [
'assigned' => $assigned,
'resolved' => $resolved,
'avgRating' => $avgRating ? round((float) $avgRating, 2) : 0,
'activities' => ActivityLog::query()->where('user_id', $this->user->id)->latest('created_at')->limit(20)->get(),
'recentRequests' => ServiceRequest::query()->where('assigned_to', $this->user->id)->latest()->limit(10)->get(),
])->title("Профиль: {$this->user->name}");
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Livewire\Users;
use App\Models\User;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
class UsersIndexPage extends Component
{
use WithPagination;
public string $search = '';
public string $role = '';
public string $status = '';
public function mount(): void
{
abort_unless(auth()->user()->isAdmin() || auth()->user()->isManager(), 403);
}
public function render()
{
$users = User::query()
->with('department')
->when($this->search !== '', function ($query): void {
$query->where(function ($q): void {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
});
})
->when($this->role !== '', fn ($q) => $q->where('role', $this->role))
->when($this->status !== '', fn ($q) => $q->where('is_active', $this->status === 'active'))
->latest()
->paginate(15);
return view('livewire.users.users-index-page', [
'users' => $users,
])->title('Пользователи');
}
}