Ver Fonte

feat(schedule): add transport and admin comment fields

- Add migration to introduce transport and admin_comment columns to schedules table
- Update ScheduleController to handle new fields in create/update operations
- Reorder schedule table columns: transport and admin comment before user note
- Add impersonate mode indicator in app layout header
- Adjust column visibility based on admin role permissions
Alexander Musikhin há 3 semanas atrás
pai
commit
12e844a32e

+ 2 - 0
app/Http/Controllers/ScheduleController.php

@@ -300,6 +300,8 @@ class ScheduleController extends Controller
                     'mafs_count' => $validated['mafs_count'],
                     'brigadier_id' => $validated['brigadier_id'],
                     'comment'   => $validated['comment'],
+                    'transport' => $validated['transport'] ?? null,
+                    'admin_comment' => $validated['admin_comment'] ?? null,
                 ]);
         }
         NotifyOrderInScheduleJob::dispatch($schedule);

+ 50 - 0
app/Http/Controllers/UserController.php

@@ -152,5 +152,55 @@ class UserController extends Controller
         return redirect()->route('user.show', $userId)->with(['success' => 'Пользователь восстановлен!']);
     }
 
+    public function impersonate(Request $request, User $user)
+    {
+        $currentUser = $request->user();
+
+        if ($currentUser->id === $user->id) {
+            return redirect()->back()->with(['danger' => 'Нельзя войти от имени самого себя!']);
+        }
+
+        if ($user->trashed()) {
+            return redirect()->back()->with(['danger' => 'Нельзя войти от имени удалённого пользователя!']);
+        }
+
+        if (session()->has('impersonator_id')) {
+            return redirect()->back()->with(['danger' => 'Вы уже вошли от имени другого пользователя.']);
+        }
+
+        $impersonatorId = $currentUser->id;
+
+        Auth::login($user);
+        $request->session()->put('impersonator_id', $impersonatorId);
+        $request->session()->regenerate();
+
+        return redirect()->route('home')->with(['success' => 'Вы вошли от имени пользователя ' . $user->name . '.']);
+    }
+
+    public function leaveImpersonation(Request $request)
+    {
+        $impersonatorId = (int) session('impersonator_id');
+
+        if (!$impersonatorId) {
+            return redirect()->back()->with(['danger' => 'Режим impersonate не активен.']);
+        }
+
+        $impersonator = User::query()->find($impersonatorId);
+
+        if (!$impersonator) {
+            Auth::logout();
+            $request->session()->invalidate();
+            $request->session()->regenerateToken();
+
+            return redirect()->route('login')->with(['danger' => 'Не удалось вернуться к исходному пользователю.']);
+        }
+
+        Auth::login($impersonator);
+        $request->session()->forget('impersonator_id');
+        $request->session()->regenerate();
+
+        return redirect()->route('user.index')->with(['success' => 'Вы вернулись в аккаунт администратора.']);
+    }
+
 
 }

+ 2 - 0
app/Http/Requests/UpdateScheduleRequest.php

@@ -25,6 +25,8 @@ class UpdateScheduleRequest extends FormRequest
             'id'                => 'nullable|exists:schedules,id',
             'installation_date' => 'required|date',
             'comment'           => 'nullable|string',
+            'transport'         => 'nullable|string',
+            'admin_comment'     => 'nullable|string',
             'area_id'           => 'nullable|exists:areas,id',
             'district_id'       => 'nullable|exists:districts,id',
             'brigadier_id'      => 'required|exists:users,id',

+ 2 - 0
app/Models/Schedule.php

@@ -23,6 +23,8 @@ class Schedule extends Model
         'mafs_count',
         'brigadier_id',
         'comment',
+        'transport',
+        'admin_comment',
     ];
 
     public function district(): BelongsTo

+ 29 - 0
database/migrations/2026_02_17_150000_add_transport_and_admin_comment_to_schedules_table.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->string('transport')->nullable()->after('comment');
+            $table->text('admin_comment')->nullable()->after('transport');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->dropColumn(['transport', 'admin_comment']);
+        });
+    }
+};

+ 10 - 0
resources/views/layouts/app.blade.php

@@ -97,6 +97,13 @@
 
                                 <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                                     <a class="dropdown-item" href="{{ route('user.profile') }}">Профиль</a>
+                                    @if(session()->has('impersonator_id'))
+                                        <a class="dropdown-item" href="{{ route('user.impersonate.leave') }}"
+                                           onclick="event.preventDefault();
+                                                     document.getElementById('leave-impersonation-form').submit();">
+                                            Выйти из impersonate
+                                        </a>
+                                    @endif
                                     <a class="dropdown-item" href="{{ route('logout') }}"
                                        onclick="event.preventDefault();
                                                      document.getElementById('logout-form').submit();">
@@ -106,6 +113,9 @@
                                     <form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
                                         @csrf
                                     </form>
+                                    <form id="leave-impersonation-form" action="{{ route('user.impersonate.leave') }}" method="POST" class="d-none">
+                                        @csrf
+                                    </form>
                                 </div>
                             </li>
                         @endguest

+ 19 - 1
resources/views/schedule/index.blade.php

@@ -87,6 +87,10 @@
                 <th>Артикулы МАФ</th>
                 <th>Кол-во позиций</th>
                 <th>Бригадир</th>
+                @if(hasRole('admin'))
+                    <th>Транспорт</th>
+                    <th>Комментарий</th>
+                @endif
                 <th>Примечание</th>
                 @if(hasRole('admin'))
                     <th></th>
@@ -129,6 +133,12 @@
                                 class="align-middle  mafs-count-{{ $schedule->id }}">{{ $schedule->mafs_count }}</td>
                             <td style="background: {{ $schedule->brigadier->color }}"
                                 class="align-middle">{{ $schedule->brigadier->name }}</td>
+                            @if(hasRole('admin'))
+                                <td style="background: {{ $schedule->brigadier->color }}"
+                                    class="align-middle transport-{{ $schedule->id }}">{{ $schedule->transport }}</td>
+                                <td style="background: {{ $schedule->brigadier->color }}"
+                                    class="align-middle admin-comment-{{ $schedule->id }}">{{ $schedule->admin_comment }}</td>
+                            @endif
                             <td style="background: {{ $schedule->brigadier->color }}"
                                 class="align-middle comment-{{ $schedule->id }}">{{ $schedule->comment }}</td>
                             @if(hasRole('admin'))
@@ -315,7 +325,9 @@
                                 @include('partials.textarea', ['name' => 'mafs', 'title' => 'МАФы', 'required' => true])
                                 @include('partials.input',  ['name' => 'mafs_count', 'title' => 'Кол-во МАФ', 'type' => 'number', 'required' => true])
                                 @include('partials.select', ['name' => 'brigadier_id', 'title' => 'Бригадир', 'options' => $brigadiers, 'required' => true, 'first_empty' => true])
-                                @include('partials.textarea', ['name' => 'comment', 'title' => 'Комментарий'])
+                                @include('partials.input',  ['name' => 'transport', 'title' => 'Транспорт'])
+                                @include('partials.textarea', ['name' => 'admin_comment', 'title' => 'Комментарий'])
+                                @include('partials.textarea', ['name' => 'comment', 'title' => 'Примечание'])
                                 <div class="text-center">
                                     <button type="submit" class="btn btn-primary btn-sm">Сохранить</button>
                                 </div>
@@ -348,6 +360,8 @@
             let scheduleBrigadier = $(this).attr('data-schedule-brigadier');
             let scheduleAddress = $(this).attr('data-schedule-address');
             let scheduleComment = $('.comment-' + scheduleId).text();
+            let scheduleTransport = $('.transport-' + scheduleId).text();
+            let scheduleAdminComment = $('.admin-comment-' + scheduleId).text();
             let scheduleMafs = $('.mafs-' + scheduleId).text();
             let scheduleObjectType = $('.object-type-' + scheduleId).text();
             let scheduleMafsCount = $('.mafs-count-' + scheduleId).text();
@@ -361,6 +375,8 @@
             $('form#scheduleEditForm select[name=area_id]').val(scheduleArea);
             $('form#scheduleEditForm select[name=brigadier_id]').val(scheduleBrigadier);
             $('form#scheduleEditForm textarea[name=comment]').text(scheduleComment);
+            $('form#scheduleEditForm input[name=transport]').val(scheduleTransport);
+            $('form#scheduleEditForm textarea[name=admin_comment]').text(scheduleAdminComment);
             $('form#scheduleEditForm textarea[name=mafs]').text(scheduleMafs);
             $('form#scheduleEditForm input[name=mafs_count]').val(scheduleMafsCount);
             $('form#scheduleEditForm input[name=address_code]').val(scheduleCode);
@@ -388,6 +404,8 @@
             $('form#scheduleEditForm select[name=area_id]').val('');
             $('form#scheduleEditForm select[name=brigadier_id]').val('');
             $('form#scheduleEditForm textarea[name=comment]').text('');
+            $('form#scheduleEditForm input[name=transport]').val('');
+            $('form#scheduleEditForm textarea[name=admin_comment]').text('');
             $('form#scheduleEditForm textarea[name=mafs]').text('');
             $('form#scheduleEditForm input[name=mafs_count]').val('');
             $('form#scheduleEditForm input[name=address_code]').val('');

+ 24 - 8
resources/views/users/edit.blade.php

@@ -26,22 +26,32 @@
                 @include('partials.select', ['name' => 'role', 'title' => 'Роль', 'options' => getRoles(), 'value' => $user->role ?? \App\Models\Role::MANAGER])
 
                 @include('partials.input', ['name' => 'color', 'title' => 'Цвет', 'value' => $user->color ?? '#FFFFFF', 'type' => 'color'])
-                @if(!is_null($user->deleted_at))
+                @if($user && !is_null($user->deleted_at))
                     <div class="col-12 text-center">
                         <div class="text-danger">ПОЛЬЗОВАТЕЛЬ УДАЛЁН!!!</div>
                         <a href="#" class="btn btn-sm btn-warning undelete">Восстановить</a>
                     </div>
                 @else
                     @include('partials.submit', ['delete' => ['form_id' => 'delete-user']])
+                    @if($user && auth()->id() !== $user->id)
+                        <div class="text-center mt-2">
+                            <a href="#" class="btn btn-sm btn-outline-primary impersonate-user">Impersonate</a>
+                        </div>
+                    @endif
                 @endif
             </form>
-            <form action="{{ route('user.undelete', $user->id) }}" method="post" class="d-none" id="undelete-user">
-                @csrf
-            </form>
-            <form action="{{ route('user.destroy', $user->id) }}" method="post" class="d-none" id="delete-user">
-                @method('DELETE')
-                @csrf
-            </form>
+            @if($user)
+                <form action="{{ route('user.undelete', $user->id) }}" method="post" class="d-none" id="undelete-user">
+                    @csrf
+                </form>
+                <form action="{{ route('user.destroy', $user->id) }}" method="post" class="d-none" id="delete-user">
+                    @method('DELETE')
+                    @csrf
+                </form>
+                <form action="{{ route('user.impersonate', $user->id) }}" method="post" class="d-none" id="impersonate-user">
+                    @csrf
+                </form>
+            @endif
         </div>
     </div>
 @endsection
@@ -54,6 +64,12 @@
             }
         });
 
+        $('.impersonate-user').on('click', function (){
+            if(confirm('Войти от имени этого пользователя?')) {
+                $('#impersonate-user').submit();
+            }
+        });
+
 
     </script>
 @endpush

+ 12 - 0
routes/web.php

@@ -50,6 +50,16 @@ Route::middleware('auth:web')->group(function () {
             session(['year' => $request->year]);
         }
 
+        $previousUrl = url()->previous();
+        $currentUrl = $request->fullUrl();
+
+        $previousHost = parse_url($previousUrl, PHP_URL_HOST);
+        $currentHost = $request->getHost();
+
+        if (!empty($previousUrl) && $previousUrl !== $currentUrl && (!$previousHost || $previousHost === $currentHost)) {
+            return redirect()->to($previousUrl);
+        }
+
         return redirect()->route('order.index');
     })->name('set-year');
 
@@ -60,6 +70,7 @@ Route::middleware('auth:web')->group(function () {
             Route::get('create', [UserController::class, 'create'])->name('user.create');
             Route::get('{user}', [UserController::class, 'show'])->name('user.show');
             Route::post('', [UserController::class, 'store'])->name('user.store');
+            Route::post('{user}/impersonate', [UserController::class, 'impersonate'])->name('user.impersonate');
 //            Route::put('{user}', [UserController::class, 'update'])->name('user.update');
             Route::delete('{user}', [UserController::class, 'destroy'])->name('user.destroy');
             Route::post('undelete/{user}', [UserController::class, 'undelete'])->name('user.undelete');
@@ -117,6 +128,7 @@ Route::middleware('auth:web')->group(function () {
     Route::get('profile', [UserController::class, 'profile'])->name('user.profile');
     Route::post('profile/store', [UserController::class, 'storeProfile'])->name('profile.store');
     Route::delete('profile', [UserController::class, 'deleteProfile'])->name('profile.delete');
+    Route::post('impersonate/leave', [UserController::class, 'leaveImpersonation'])->name('user.impersonate.leave');
 
     Route::get('get-filters', [FilterController::class, 'getFilters'])->name('getFilters');