first&last
This commit is contained in:
@@ -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('Вход в систему');
|
||||
}
|
||||
}
|
||||
@@ -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('Справочник: Категории оборудования');
|
||||
}
|
||||
}
|
||||
@@ -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('Справочник: Типы заявок');
|
||||
}
|
||||
}
|
||||
@@ -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('Уведомления');
|
||||
}
|
||||
}
|
||||
@@ -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('Отчет: Общий');
|
||||
}
|
||||
}
|
||||
@@ -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('Отчет: Эффективность техников');
|
||||
}
|
||||
}
|
||||
@@ -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 ? 'Редактирование заявки' : 'Создание заявки');
|
||||
}
|
||||
}
|
||||
@@ -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('Заявки');
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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('Журнал действий');
|
||||
}
|
||||
}
|
||||
@@ -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('Системные настройки');
|
||||
}
|
||||
}
|
||||
@@ -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('Мои задачи');
|
||||
}
|
||||
}
|
||||
@@ -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 ? 'Редактирование пользователя' : 'Создание пользователя');
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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('Пользователи');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user