first&last
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,59 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
@@ -0,0 +1,66 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com/)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[WebReinvent](https://webreinvent.com/)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
||||
- **[Cyber-Duck](https://cyber-duck.co.uk)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Jump24](https://jump24.co.uk)**
|
||||
- **[Redberry](https://redberry.international/laravel/)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
- **[byte5](https://byte5.de)**
|
||||
- **[OP.GG](https://op.gg)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user