first&last
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckSlaRequestsCommand extends Command
|
||||
{
|
||||
protected $signature = 'requests:check-sla';
|
||||
|
||||
protected $description = 'Проверка SLA и отправка предупреждений по заявкам';
|
||||
|
||||
public function __construct(private readonly NotificationService $notificationService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$managers = User::query()->whereIn('role', ['admin', 'manager'])->get();
|
||||
|
||||
ServiceRequest::query()
|
||||
->with(['status', 'assignee'])
|
||||
->open()
|
||||
->whereNotNull('deadline_at')
|
||||
->whereBetween('deadline_at', [now(), now()->addHour()])
|
||||
->each(fn (ServiceRequest $request) => $this->notificationService->notifySlaWarning($request, $managers));
|
||||
|
||||
ServiceRequest::query()
|
||||
->with(['status', 'assignee'])
|
||||
->overdue()
|
||||
->each(fn (ServiceRequest $request) => $this->notificationService->notifyOverdue($request, $managers));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
$schedule->command('requests:check-sla')->everyFiveMinutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum EquipmentCondition: string
|
||||
{
|
||||
case GOOD = 'good';
|
||||
case FAIR = 'fair';
|
||||
case POOR = 'poor';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::GOOD => 'Хорошее',
|
||||
self::FAIR => 'Удовлетворительное',
|
||||
self::POOR => 'Плохое',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StatusSlug: string
|
||||
{
|
||||
case NEW = 'new';
|
||||
case IN_PROGRESS = 'in_progress';
|
||||
case WAITING = 'waiting';
|
||||
case RESOLVED = 'resolved';
|
||||
case CLOSED = 'closed';
|
||||
case CANCELLED = 'cancelled';
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserRole: string
|
||||
{
|
||||
case ADMIN = 'admin';
|
||||
case MANAGER = 'manager';
|
||||
case TECHNICIAN = 'technician';
|
||||
case CLIENT = 'client';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ADMIN => 'Администратор',
|
||||
self::MANAGER => 'Менеджер',
|
||||
self::TECHNICIAN => 'Техник',
|
||||
self::CLIENT => 'Клиент',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RequestAssigned
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public ServiceRequest $request,
|
||||
public User $actor
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RequestCreated
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public ServiceRequest $request,
|
||||
public User $actor
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RequestStatusChanged
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public ServiceRequest $request,
|
||||
public User $actor,
|
||||
public int $oldStatusId,
|
||||
public int $newStatusId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* The list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class CsvExport
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $headers
|
||||
* @param array<int, array<int, mixed>> $rows
|
||||
*/
|
||||
public static function download(string $filename, array $headers, array $rows): StreamedResponse
|
||||
{
|
||||
return response()->streamDownload(function () use ($headers, $rows): void {
|
||||
$handle = fopen('php://output', 'wb');
|
||||
|
||||
if ($handle === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// UTF-8 BOM improves Russian text rendering in Excel.
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($handle, self::normalizeRow($headers), ';');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($handle, self::normalizeRow($row), ';');
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $row
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private static function normalizeRow(array $row): array
|
||||
{
|
||||
return array_map(static function (mixed $value): mixed {
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return str_replace(["\r\n", "\r"], "\n", $value);
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}, $row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\RequestAttachment;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AttachmentController extends Controller
|
||||
{
|
||||
public function download(ServiceRequest $request, RequestAttachment $attachment): Response
|
||||
{
|
||||
abort_unless($attachment->request_id === $request->id, 404);
|
||||
|
||||
Gate::authorize('view', $request);
|
||||
|
||||
if (!Storage::disk('local')->exists($attachment->path)) {
|
||||
abort(404, 'Файл не найден.');
|
||||
}
|
||||
|
||||
return response()->download(
|
||||
Storage::disk('local')->path($attachment->path),
|
||||
$attachment->filename
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
public function create()
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate(['email' => ['required', 'email']]);
|
||||
|
||||
$status = Password::sendResetLink($request->only('email'));
|
||||
|
||||
return $status === Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', 'min:8'],
|
||||
]);
|
||||
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request): void {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->string('password')->toString()),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
return $status === Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withErrors(['email' => [__($status)]]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's middleware aliases.
|
||||
*
|
||||
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'active_user' => \App\Http\Middleware\EnsureUserIsActive::class,
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
|
||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*/
|
||||
protected function redirectTo(Request $request): ?string
|
||||
{
|
||||
return $request->expectsJson() ? null : route('login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureUserIsActive
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user && !$user->is_active) {
|
||||
auth()->logout();
|
||||
|
||||
return redirect()->route('login')->withErrors([
|
||||
'email' => 'Ваш аккаунт отключен. Обратитесь к администратору.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
||||
|
||||
class PreventRequestsDuringMaintenance extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be reachable while maintenance mode is enabled.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||
{
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string ...$roles): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
abort(401);
|
||||
}
|
||||
|
||||
if (!in_array($user->role->value, $roles, true)) {
|
||||
abort(403, 'Недостаточно прав для доступа к разделу.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array<int, string|null>
|
||||
*/
|
||||
public function hosts(): array
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array<int, string>|string|null
|
||||
*/
|
||||
protected $proxies;
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
|
||||
|
||||
class ValidateSignature extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the query string parameters that should be ignored.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
// 'fbclid',
|
||||
// 'utm_campaign',
|
||||
// 'utm_content',
|
||||
// 'utm_medium',
|
||||
// 'utm_source',
|
||||
// 'utm_term',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'remember' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Directory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DepartmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$department = $this->route('department');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255', 'unique:departments,name'.($department ? ','.$department->id : '')],
|
||||
'description' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Directory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class EquipmentCategoryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$category = $this->route('equipment_category');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'string', 'max:255', Rule::unique('equipment_categories', 'slug')->ignore($category)],
|
||||
'description' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Directory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class EquipmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$equipment = $this->route('equipment');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'serial_number' => ['required', 'string', Rule::unique('equipment', 'serial_number')->ignore($equipment)],
|
||||
'inventory_number' => ['required', 'string', Rule::unique('equipment', 'inventory_number')->ignore($equipment)],
|
||||
'category_id' => ['required', 'exists:equipment_categories,id'],
|
||||
'client_id' => ['nullable', 'exists:users,id'],
|
||||
'purchase_date' => ['nullable', 'date'],
|
||||
'warranty_until' => ['nullable', 'date'],
|
||||
'condition' => ['required', Rule::in(['good', 'fair', 'poor'])],
|
||||
'notes' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Directory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PriorityRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$priority = $this->route('priority');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'color_hex' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
|
||||
'level' => ['required', 'integer', 'between:1,4', Rule::unique('priorities', 'level')->ignore($priority)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Directory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RequestTypeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$type = $this->route('request_type');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255', Rule::unique('request_types', 'name')->ignore($type)],
|
||||
'description' => ['nullable', 'string'],
|
||||
'sla_hours' => ['required', 'integer', 'min:1', 'max:720'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Directory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$status = $this->route('status');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'color_hex' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
|
||||
'slug' => ['required', 'string', Rule::unique('statuses', 'slug')->ignore($status)],
|
||||
'is_final' => ['nullable', 'boolean'],
|
||||
'sort_order' => ['required', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Profile;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProfileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($this->user()?->id)],
|
||||
'phone' => ['nullable', 'string', 'max:32'],
|
||||
'password' => ['nullable', 'string', 'confirmed', 'min:8'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Request;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RateServiceRequestRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_rating' => ['required', 'integer', 'between:1,5'],
|
||||
'client_comment' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Request;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreCommentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => ['required', 'string', 'max:5000'],
|
||||
'is_internal' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Request;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreServiceRequestRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['required', 'string'],
|
||||
'type_id' => ['required', 'exists:request_types,id'],
|
||||
'priority_id' => ['required', 'exists:priorities,id'],
|
||||
'status_id' => ['nullable', 'exists:statuses,id'],
|
||||
'client_id' => ['required', 'exists:users,id'],
|
||||
'assigned_to' => ['nullable', 'exists:users,id'],
|
||||
'equipment_id' => ['nullable', 'exists:equipment,id'],
|
||||
'location' => ['nullable', 'string', 'max:255'],
|
||||
'planned_at' => ['nullable', 'date'],
|
||||
'deadline_at' => ['nullable', 'date'],
|
||||
'resolution_notes' => ['nullable', 'string'],
|
||||
'attachments' => ['nullable', 'array', 'max:5'],
|
||||
'attachments.*' => ['file', 'max:10240'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Request;
|
||||
|
||||
class UpdateServiceRequestRequest extends StoreServiceRequestRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = parent::rules();
|
||||
$rules['title'][0] = 'sometimes';
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\System;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateSettingsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => ['required', 'string', 'max:255'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'default_sla_hours' => ['required', 'integer', 'min:1', 'max:720'],
|
||||
'maintenance_mode' => ['nullable', 'boolean'],
|
||||
'notify_assigned' => ['nullable', 'boolean'],
|
||||
'notify_status_changed' => ['nullable', 'boolean'],
|
||||
'notify_comment_added' => ['nullable', 'boolean'],
|
||||
'notify_overdue' => ['nullable', 'boolean'],
|
||||
'notify_sla_warning' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
'role' => ['required', Rule::in(['admin', 'manager', 'technician', 'client'])],
|
||||
'department_id' => ['nullable', 'exists:departments,id'],
|
||||
'phone' => ['nullable', 'string', 'max:32'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$user = $this->route('user');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user)],
|
||||
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||
'role' => ['required', Rule::in(['admin', 'manager', 'technician', 'client'])],
|
||||
'department_id' => ['nullable', 'exists:departments,id'],
|
||||
'phone' => ['nullable', 'string', 'max:32'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ActivityLogResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'user' => $this->user?->name,
|
||||
'action' => $this->action,
|
||||
'entity' => class_basename($this->model_type),
|
||||
'model_id' => $this->model_id,
|
||||
'description' => $this->description,
|
||||
'ip_address' => $this->ip_address,
|
||||
'created_at' => $this->created_at?->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ServiceRequestResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'ticket_number' => $this->ticket_number,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'type' => $this->type?->name,
|
||||
'priority' => $this->priority?->name,
|
||||
'priority_color' => $this->priority?->color_hex,
|
||||
'status' => $this->status?->name,
|
||||
'status_slug' => $this->status?->slug,
|
||||
'status_color' => $this->status?->color_hex,
|
||||
'client' => $this->client?->name,
|
||||
'assignee' => $this->assignee?->name,
|
||||
'deadline_at' => $this->deadline_at?->toDateTimeString(),
|
||||
'created_at' => $this->created_at?->toDateTimeString(),
|
||||
'updated_at' => $this->updated_at?->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'role' => $this->role?->value,
|
||||
'role_label' => $this->role?->label(),
|
||||
'phone' => $this->phone,
|
||||
'department' => $this->department?->name,
|
||||
'is_active' => $this->is_active,
|
||||
'last_login_at' => $this->last_login_at?->toDateTimeString(),
|
||||
'created_at' => $this->created_at?->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\RequestAssigned;
|
||||
use App\Services\ActivityLogService;
|
||||
|
||||
class LogRequestAssigned
|
||||
{
|
||||
public function __construct(private readonly ActivityLogService $activityLogService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(RequestAssigned $event): void
|
||||
{
|
||||
$assignee = $event->request->assignee?->name ?? 'не назначен';
|
||||
|
||||
$this->activityLogService->log(
|
||||
$event->actor,
|
||||
$event->request,
|
||||
$event->request->id,
|
||||
'request_assigned',
|
||||
"Заявка {$event->request->ticket_number} назначена на {$assignee}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\RequestCreated;
|
||||
use App\Services\ActivityLogService;
|
||||
|
||||
class LogRequestCreated
|
||||
{
|
||||
public function __construct(private readonly ActivityLogService $activityLogService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(RequestCreated $event): void
|
||||
{
|
||||
$this->activityLogService->log(
|
||||
$event->actor,
|
||||
$event->request,
|
||||
$event->request->id,
|
||||
'request_created',
|
||||
"Создана заявка {$event->request->ticket_number}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\RequestStatusChanged;
|
||||
use App\Services\ActivityLogService;
|
||||
|
||||
class LogRequestStatusChanged
|
||||
{
|
||||
public function __construct(private readonly ActivityLogService $activityLogService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(RequestStatusChanged $event): void
|
||||
{
|
||||
$this->activityLogService->log(
|
||||
$event->actor,
|
||||
$event->request,
|
||||
$event->request->id,
|
||||
'request_status_changed',
|
||||
"Статус заявки {$event->request->ticket_number} изменен"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\RequestAssigned;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class SendAssignedNotification implements ShouldQueue
|
||||
{
|
||||
public function __construct(private readonly NotificationService $notificationService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(RequestAssigned $event): void
|
||||
{
|
||||
$this->notificationService->notifyRequestAssigned($event->request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\RequestStatusChanged;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class SendStatusChangedNotification implements ShouldQueue
|
||||
{
|
||||
public function __construct(private readonly NotificationService $notificationService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(RequestStatusChanged $event): void
|
||||
{
|
||||
$this->notificationService->notifyStatusChanged($event->request);
|
||||
}
|
||||
}
|
||||
@@ -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('Пользователи');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'model_type',
|
||||
'model_id',
|
||||
'action',
|
||||
'description',
|
||||
'ip_address',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Department extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\EquipmentCondition;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Equipment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'equipment';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'serial_number',
|
||||
'inventory_number',
|
||||
'category_id',
|
||||
'client_id',
|
||||
'purchase_date',
|
||||
'warranty_until',
|
||||
'condition',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'purchase_date' => 'date',
|
||||
'warranty_until' => 'date',
|
||||
'condition' => EquipmentCondition::class,
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EquipmentCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'client_id');
|
||||
}
|
||||
|
||||
public function requests(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceRequest::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function scopeWarrantyExpired(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNotNull('warranty_until')->whereDate('warranty_until', '<', now());
|
||||
}
|
||||
|
||||
public function scopeWarrantyExpiringSoon(Builder $query, int $days = 30): Builder
|
||||
{
|
||||
return $query
|
||||
->whereNotNull('warranty_until')
|
||||
->whereDate('warranty_until', '>=', now())
|
||||
->whereDate('warranty_until', '<=', now()->addDays($days));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class EquipmentCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
];
|
||||
|
||||
public function equipment(): HasMany
|
||||
{
|
||||
return $this->hasMany(Equipment::class, 'category_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Priority extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'color_hex',
|
||||
'level',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'level' => 'integer',
|
||||
];
|
||||
|
||||
public function requests(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceRequest::class, 'priority_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RequestAttachment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'request_id',
|
||||
'user_id',
|
||||
'filename',
|
||||
'path',
|
||||
'size',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'size' => 'integer',
|
||||
];
|
||||
|
||||
public function request(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ServiceRequest::class, 'request_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RequestComment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'request_id',
|
||||
'user_id',
|
||||
'body',
|
||||
'is_internal',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_internal' => 'boolean',
|
||||
];
|
||||
|
||||
public function request(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ServiceRequest::class, 'request_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class RequestType extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'sla_hours',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sla_hours' => 'integer',
|
||||
];
|
||||
|
||||
public function requests(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceRequest::class, 'type_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\StatusSlug;
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ServiceRequest extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'requests';
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_number',
|
||||
'title',
|
||||
'description',
|
||||
'type_id',
|
||||
'priority_id',
|
||||
'status_id',
|
||||
'client_id',
|
||||
'assigned_to',
|
||||
'equipment_id',
|
||||
'location',
|
||||
'planned_at',
|
||||
'deadline_at',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'resolution_notes',
|
||||
'client_rating',
|
||||
'client_comment',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'planned_at' => 'datetime',
|
||||
'deadline_at' => 'datetime',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'client_rating' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function type(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RequestType::class, 'type_id');
|
||||
}
|
||||
|
||||
public function priority(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Priority::class, 'priority_id');
|
||||
}
|
||||
|
||||
public function status(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Status::class, 'status_id');
|
||||
}
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'client_id');
|
||||
}
|
||||
|
||||
public function assignee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_to');
|
||||
}
|
||||
|
||||
public function equipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Equipment::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(RequestComment::class, 'request_id')->latest();
|
||||
}
|
||||
|
||||
public function attachments(): HasMany
|
||||
{
|
||||
return $this->hasMany(RequestAttachment::class, 'request_id')->latest();
|
||||
}
|
||||
|
||||
public function scopeOpen(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('status', fn (Builder $status) => $status->where('is_final', false));
|
||||
}
|
||||
|
||||
public function scopeOverdue(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->whereNotNull('deadline_at')
|
||||
->where('deadline_at', '<', now())
|
||||
->whereHas('status', fn (Builder $status) => $status->where('is_final', false));
|
||||
}
|
||||
|
||||
public function scopeMine(Builder $query, User $user): Builder
|
||||
{
|
||||
if ($user->isClient()) {
|
||||
return $query->where('client_id', $user->id);
|
||||
}
|
||||
|
||||
if ($user->isTechnician()) {
|
||||
return $query->where('assigned_to', $user->id);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function scopeForRole(Builder $query, User $user): Builder
|
||||
{
|
||||
return match ($user->role) {
|
||||
UserRole::CLIENT => $query->where('client_id', $user->id),
|
||||
UserRole::TECHNICIAN => $query->where('assigned_to', $user->id),
|
||||
default => $query,
|
||||
};
|
||||
}
|
||||
|
||||
public function isOverdue(): bool
|
||||
{
|
||||
if ($this->deadline_at === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->deadline_at->isPast() && !$this->status?->is_final;
|
||||
}
|
||||
|
||||
public function hasStatus(StatusSlug $slug): bool
|
||||
{
|
||||
return $this->status?->slug === $slug->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'value',
|
||||
'type',
|
||||
];
|
||||
|
||||
public static function getValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$setting = static::query()->where('key', $key)->first();
|
||||
|
||||
if (!$setting) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $setting->castValue();
|
||||
}
|
||||
|
||||
public static function setValue(string $key, mixed $value, string $type = 'string'): self
|
||||
{
|
||||
return static::query()->updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => static::serializeValue($value, $type), 'type' => $type]
|
||||
);
|
||||
}
|
||||
|
||||
public function castValue(): mixed
|
||||
{
|
||||
return match ($this->type) {
|
||||
'integer' => (int) $this->value,
|
||||
'boolean' => filter_var($this->value, FILTER_VALIDATE_BOOLEAN),
|
||||
'json' => $this->value ? json_decode($this->value, true, 512, JSON_THROW_ON_ERROR) : [],
|
||||
default => $this->value,
|
||||
};
|
||||
}
|
||||
|
||||
private static function serializeValue(mixed $value, string $type): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($type) {
|
||||
'json' => json_encode($value, JSON_THROW_ON_ERROR),
|
||||
'boolean' => $value ? '1' : '0',
|
||||
default => (string) $value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Status extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'color_hex',
|
||||
'slug',
|
||||
'is_final',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_final' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function requests(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceRequest::class, 'status_id');
|
||||
}
|
||||
|
||||
public function scopeFinal(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_final', true);
|
||||
}
|
||||
|
||||
public function scopeActiveFlow(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_final', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens;
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
'phone',
|
||||
'avatar',
|
||||
'department_id',
|
||||
'is_active',
|
||||
'last_login_at',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'last_login_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
'role' => UserRole::class,
|
||||
];
|
||||
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function clientRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceRequest::class, 'client_id');
|
||||
}
|
||||
|
||||
public function assignedRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceRequest::class, 'assigned_to');
|
||||
}
|
||||
|
||||
public function requestComments(): HasMany
|
||||
{
|
||||
return $this->hasMany(RequestComment::class);
|
||||
}
|
||||
|
||||
public function requestAttachments(): HasMany
|
||||
{
|
||||
return $this->hasMany(RequestAttachment::class);
|
||||
}
|
||||
|
||||
public function activityLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(ActivityLog::class);
|
||||
}
|
||||
|
||||
public function equipment(): HasMany
|
||||
{
|
||||
return $this->hasMany(Equipment::class, 'client_id');
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === UserRole::ADMIN;
|
||||
}
|
||||
|
||||
public function isManager(): bool
|
||||
{
|
||||
return $this->role === UserRole::MANAGER;
|
||||
}
|
||||
|
||||
public function isTechnician(): bool
|
||||
{
|
||||
return $this->role === UserRole::TECHNICIAN;
|
||||
}
|
||||
|
||||
public function isClient(): bool
|
||||
{
|
||||
return $this->role === UserRole::CLIENT;
|
||||
}
|
||||
|
||||
public function isStaff(): bool
|
||||
{
|
||||
return $this->isAdmin() || $this->isManager() || $this->isTechnician();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class RequestAssignedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(private readonly ServiceRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Новая назначенная заявка')
|
||||
->line("Вам назначена заявка {$this->request->ticket_number}.")
|
||||
->action('Открыть заявку', route('requests.show', $this->request));
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Назначена новая заявка',
|
||||
'message' => "Вам назначена заявка {$this->request->ticket_number}",
|
||||
'request_id' => $this->request->id,
|
||||
'url' => route('requests.show', $this->request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class RequestCommentAddedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(private readonly ServiceRequest $request, private readonly User $author)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Новый комментарий к заявке')
|
||||
->line("{$this->author->name} добавил комментарий к заявке {$this->request->ticket_number}.")
|
||||
->action('Открыть заявку', route('requests.show', $this->request));
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Новый комментарий',
|
||||
'message' => "{$this->author->name} добавил комментарий к {$this->request->ticket_number}",
|
||||
'request_id' => $this->request->id,
|
||||
'url' => route('requests.show', $this->request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class RequestOverdueNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(private readonly ServiceRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Просроченная заявка')
|
||||
->line("Заявка {$this->request->ticket_number} просрочена.")
|
||||
->action('Открыть заявку', route('requests.show', $this->request));
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Просроченная заявка',
|
||||
'message' => "Заявка {$this->request->ticket_number} просрочена",
|
||||
'request_id' => $this->request->id,
|
||||
'url' => route('requests.show', $this->request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class RequestStatusChangedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(private readonly ServiceRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Статус заявки изменен')
|
||||
->line("Статус заявки {$this->request->ticket_number} изменился на «{$this->request->status?->name}»." )
|
||||
->action('Открыть заявку', route('my-requests.show', $this->request));
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Изменение статуса заявки',
|
||||
'message' => "Заявка {$this->request->ticket_number}: {$this->request->status?->name}",
|
||||
'request_id' => $this->request->id,
|
||||
'url' => route('my-requests.show', $this->request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ServiceRequest;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class SlaWarningNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(private readonly ServiceRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('SLA предупреждение')
|
||||
->line("До дедлайна заявки {$this->request->ticket_number} осталось менее часа.")
|
||||
->action('Открыть заявку', route('requests.show', $this->request));
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => 'Предупреждение SLA',
|
||||
'message' => "До дедлайна заявки {$this->request->ticket_number} осталось менее часа",
|
||||
'request_id' => $this->request->id,
|
||||
'url' => route('requests.show', $this->request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
|
||||
class ActivityLogPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function view(User $user, ActivityLog $activityLog): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\User;
|
||||
|
||||
class DepartmentPolicy
|
||||
{
|
||||
public function before(User $user, string $ability): bool|null
|
||||
{
|
||||
return $user->isAdmin() ? true : null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $user, Department $department): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, Department $department): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, Department $department): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\EquipmentCategory;
|
||||
use App\Models\User;
|
||||
|
||||
class EquipmentCategoryPolicy
|
||||
{
|
||||
public function before(User $user, string $ability): bool|null
|
||||
{
|
||||
return $user->isAdmin() ? true : null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $user, EquipmentCategory $equipmentCategory): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, EquipmentCategory $equipmentCategory): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, EquipmentCategory $equipmentCategory): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Equipment;
|
||||
use App\Models\User;
|
||||
|
||||
class EquipmentPolicy
|
||||
{
|
||||
public function before(User $user, string $ability): bool|null
|
||||
{
|
||||
return $user->isAdmin() ? true : null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $user, Equipment $equipment): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, Equipment $equipment): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, Equipment $equipment): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Priority;
|
||||
use App\Models\User;
|
||||
|
||||
class PriorityPolicy
|
||||
{
|
||||
public function before(User $user, string $ability): bool|null
|
||||
{
|
||||
return $user->isAdmin() ? true : null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $user, Priority $priority): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, Priority $priority): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, Priority $priority): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\RequestType;
|
||||
use App\Models\User;
|
||||
|
||||
class RequestTypePolicy
|
||||
{
|
||||
public function before(User $user, string $ability): bool|null
|
||||
{
|
||||
return $user->isAdmin() ? true : null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $user, RequestType $requestType): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, RequestType $requestType): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, RequestType $requestType): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\StatusSlug;
|
||||
use App\Models\ServiceRequest;
|
||||
use App\Models\User;
|
||||
|
||||
class ServiceRequestPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view(User $user, ServiceRequest $serviceRequest): bool
|
||||
{
|
||||
if ($user->isAdmin() || $user->isManager()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user->isTechnician()) {
|
||||
return $serviceRequest->assigned_to === $user->id;
|
||||
}
|
||||
|
||||
return $serviceRequest->client_id === $user->id;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->isAdmin() || $user->isManager() || $user->isClient();
|
||||
}
|
||||
|
||||
public function update(User $user, ServiceRequest $serviceRequest): bool
|
||||
{
|
||||
if ($user->isAdmin() || $user->isManager()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user->isTechnician()) {
|
||||
return $serviceRequest->assigned_to === $user->id;
|
||||
}
|
||||
|
||||
return $serviceRequest->client_id === $user->id
|
||||
&& $serviceRequest->status?->slug === StatusSlug::NEW->value;
|
||||
}
|
||||
|
||||
public function delete(User $user, ServiceRequest $serviceRequest): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function addInternalComment(User $user, ServiceRequest $serviceRequest): bool
|
||||
{
|
||||
if (!$user->isStaff()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->view($user, $serviceRequest);
|
||||
}
|
||||
|
||||
public function rate(User $user, ServiceRequest $serviceRequest): bool
|
||||
{
|
||||
if (!$user->isClient()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $serviceRequest->client_id === $user->id
|
||||
&& in_array($serviceRequest->status?->slug, [StatusSlug::RESOLVED->value, StatusSlug::CLOSED->value], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
|
||||
class SettingPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function update(User $user, Setting $setting): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Status;
|
||||
use App\Models\User;
|
||||
|
||||
class StatusPolicy
|
||||
{
|
||||
public function before(User $user, string $ability): bool|null
|
||||
{
|
||||
return $user->isAdmin() ? true : null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $user, Status $status): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, Status $status): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, Status $status): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user