1
0

4 Коммитууд d5c6b05fb5 ... a58a3fd441

Эзэн SHA1 Мессеж Огноо
  Alexander Musikhin a58a3fd441 feat(maf): add bulk stock status update and role-based export 1 долоо хоног өмнө
  Alexander Musikhin 6ebecceec3 refactor(ui): improve table dropdown z-index handling and trim whitespace 1 долоо хоног өмнө
  Alexander Musikhin b9f18d8fa0 refactor(query): replace withoutGlobalScopes with specific scope bypass 1 долоо хоног өмнө
  Alexander Musikhin aa7cd08d22 new roles 1 долоо хоног өмнө
45 өөрчлөгдсөн 592 нэмэгдсэн , 206 устгасан
  1. 15 15
      app/Console/Commands/ClearYearData.php
  2. 4 1
      app/Helpers/roles.php
  3. 9 9
      app/Http/Controllers/ClearDataController.php
  4. 1 1
      app/Http/Controllers/ImportController.php
  5. 33 1
      app/Http/Controllers/MafOrderController.php
  6. 1 1
      app/Http/Controllers/OrderController.php
  7. 1 1
      app/Http/Controllers/ProductController.php
  8. 16 2
      app/Http/Controllers/ProductSKUController.php
  9. 45 1
      app/Http/Controllers/ReclamationController.php
  10. 2 2
      app/Http/Controllers/ScheduleController.php
  11. 9 9
      app/Http/Controllers/YearDataController.php
  12. 2 1
      app/Http/Middleware/EnsureUserHasRole.php
  13. 1 2
      app/Http/Requests/StoreReclamationSparePartsRequest.php
  14. 10 10
      app/Jobs/ClearYearDataJob.php
  15. 1 1
      app/Jobs/ExportMafJob.php
  16. 1 1
      app/Jobs/NotifyOrderInScheduleJob.php
  17. 2 2
      app/Models/MafOrder.php
  18. 1 1
      app/Models/MafView.php
  19. 3 3
      app/Models/ProductSKU.php
  20. 2 2
      app/Models/Reclamation.php
  21. 19 4
      app/Models/Role.php
  22. 7 7
      app/Services/Export/ExportYearDataService.php
  23. 80 6
      app/Services/ExportMafService.php
  24. 2 1
      app/Services/ExportOneOrderService.php
  25. 1 1
      app/Services/ExportOrdersService.php
  26. 2 2
      app/Services/ExportReclamationsService.php
  27. 4 4
      app/Services/GenerateDocumentsService.php
  28. 1 1
      app/Services/Import/ImportMafOrdersService.php
  29. 25 22
      app/Services/Import/ImportYearDataService.php
  30. 1 1
      app/Services/ImportCatalogService.php
  31. 1 1
      app/Services/ImportMafsService.php
  32. 42 37
      app/Services/ImportOrdersService.php
  33. 4 4
      app/Services/ImportReclamationsService.php
  34. 14 0
      database/factories/UserFactory.php
  35. 32 0
      database/migrations/2026_02_26_120000_trim_whitespace_in_order_related_fields.php
  36. 9 1
      resources/sass/app.scss
  37. 3 1
      resources/views/layouts/menu.blade.php
  38. 45 0
      resources/views/maf_orders/index.blade.php
  39. 14 12
      resources/views/orders/show.blade.php
  40. 9 8
      resources/views/products_sku/index.blade.php
  41. 29 21
      resources/views/reclamations/edit.blade.php
  42. 10 5
      routes/web.php
  43. BIN
      templates/Passport.xlsx
  44. 64 0
      tests/Feature/MafOrderControllerTest.php
  45. 15 1
      tests/Unit/Helpers/RolesHelperTest.php

+ 15 - 15
app/Console/Commands/ClearYearData.php

@@ -65,10 +65,10 @@ class ClearYearData extends Command
 
 
     private function collectStats(int $year): array
     private function collectStats(int $year): array
     {
     {
-        $orderIds = Order::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $mafOrderIds = MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productSkuIds = ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $mafOrderIds = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
 
 
         $reclamationCount = Reclamation::whereIn('order_id', $orderIds)->count();
         $reclamationCount = Reclamation::whereIn('order_id', $orderIds)->count();
 
 
@@ -121,7 +121,7 @@ class ClearYearData extends Command
 
 
         // Сертификаты продуктов
         // Сертификаты продуктов
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            Product::withoutGlobalScopes()->withTrashed()
+            Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productIds)
                 ->whereIn('id', $productIds)
                 ->whereNotNull('certificate_id')
                 ->whereNotNull('certificate_id')
                 ->pluck('certificate_id')
                 ->pluck('certificate_id')
@@ -129,7 +129,7 @@ class ClearYearData extends Command
 
 
         // Паспорта SKU
         // Паспорта SKU
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            ProductSKU::withoutGlobalScopes()->withTrashed()
+            ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productSkuIds)
                 ->whereIn('id', $productSkuIds)
                 ->whereNotNull('passport_id')
                 ->whereNotNull('passport_id')
                 ->pluck('passport_id')
                 ->pluck('passport_id')
@@ -145,10 +145,10 @@ class ClearYearData extends Command
 
 
     private function clearData(int $year): void
     private function clearData(int $year): void
     {
     {
-        $orderIds = Order::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $mafOrderIds = MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productSkuIds = ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $mafOrderIds = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
         $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
         $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
 
 
         // Собираем файлы до удаления связей
         // Собираем файлы до удаления связей
@@ -173,20 +173,20 @@ class ClearYearData extends Command
         Schedule::whereIn('order_id', $orderIds)->delete();
         Schedule::whereIn('order_id', $orderIds)->delete();
 
 
         $this->info('Удаление SKU продуктов...');
         $this->info('Удаление SKU продуктов...');
-        ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $year)->forceDelete();
+        ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->forceDelete();
 
 
         $this->info('Удаление заказов...');
         $this->info('Удаление заказов...');
-        Order::withoutGlobalScopes()->withTrashed()->where('year', $year)->forceDelete();
+        Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->forceDelete();
 
 
         $this->info('Удаление заказов МАФ...');
         $this->info('Удаление заказов МАФ...');
-        MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $year)->forceDelete();
+        MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->forceDelete();
 
 
         $this->info('Удаление продуктов...');
         $this->info('Удаление продуктов...');
         // Сначала обнуляем certificate_id чтобы избежать проблем с FK
         // Сначала обнуляем certificate_id чтобы избежать проблем с FK
-        Product::withoutGlobalScopes()->withTrashed()
+        Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->whereIn('id', $productIds)
             ->whereIn('id', $productIds)
             ->update(['certificate_id' => null]);
             ->update(['certificate_id' => null]);
-        Product::withoutGlobalScopes()->withTrashed()->where('year', $year)->forceDelete();
+        Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->forceDelete();
 
 
         $this->info('Удаление ТТН...');
         $this->info('Удаление ТТН...');
         // Обнуляем file_id перед удалением
         // Обнуляем file_id перед удалением

+ 4 - 1
app/Helpers/roles.php

@@ -18,9 +18,12 @@ if(!function_exists('hasRole')){
     function hasRole($roles, $user = null) : bool
     function hasRole($roles, $user = null) : bool
     {
     {
         if(!$user) $user = auth()->user();
         if(!$user) $user = auth()->user();
+        if(!$user) return false;
 
 
         $roles = explode(',', $roles);
         $roles = explode(',', $roles);
-        return ($user && in_array($user->role, $roles));
+        $effectiveRoles = Role::effectiveRoles($user->role);
+
+        return count(array_intersect($roles, $effectiveRoles)) > 0;
     }
     }
 }
 }
 
 

+ 9 - 9
app/Http/Controllers/ClearDataController.php

@@ -60,17 +60,17 @@ class ClearDataController extends Controller
     {
     {
         $years = [];
         $years = [];
 
 
-        $orderYears = Order::withoutGlobalScopes()->withTrashed()
+        $orderYears = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->selectRaw('DISTINCT year')
             ->selectRaw('DISTINCT year')
             ->pluck('year')
             ->pluck('year')
             ->toArray();
             ->toArray();
 
 
-        $productYears = Product::withoutGlobalScopes()->withTrashed()
+        $productYears = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->selectRaw('DISTINCT year')
             ->selectRaw('DISTINCT year')
             ->pluck('year')
             ->pluck('year')
             ->toArray();
             ->toArray();
 
 
-        $mafOrderYears = MafOrder::withoutGlobalScopes()->withTrashed()
+        $mafOrderYears = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->selectRaw('DISTINCT year')
             ->selectRaw('DISTINCT year')
             ->pluck('year')
             ->pluck('year')
             ->toArray();
             ->toArray();
@@ -91,10 +91,10 @@ class ClearDataController extends Controller
 
 
     private function collectStats(int $year): array
     private function collectStats(int $year): array
     {
     {
-        $orderIds = Order::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $mafOrderIds = MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productSkuIds = ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $mafOrderIds = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
 
 
         $reclamationCount = Reclamation::whereIn('order_id', $orderIds)->count();
         $reclamationCount = Reclamation::whereIn('order_id', $orderIds)->count();
         $fileIds = $this->collectFileIds($year, $orderIds, $productIds, $productSkuIds);
         $fileIds = $this->collectFileIds($year, $orderIds, $productIds, $productSkuIds);
@@ -142,14 +142,14 @@ class ClearDataController extends Controller
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            Product::withoutGlobalScopes()->withTrashed()
+            Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productIds)
                 ->whereIn('id', $productIds)
                 ->whereNotNull('certificate_id')
                 ->whereNotNull('certificate_id')
                 ->pluck('certificate_id')
                 ->pluck('certificate_id')
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            ProductSKU::withoutGlobalScopes()->withTrashed()
+            ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productSkuIds)
                 ->whereIn('id', $productSkuIds)
                 ->whereNotNull('passport_id')
                 ->whereNotNull('passport_id')
                 ->pluck('passport_id')
                 ->pluck('passport_id')

+ 1 - 1
app/Http/Controllers/ImportController.php

@@ -40,7 +40,7 @@ class ImportController extends Controller
         $this->createDateFilters($model, 'created_at', 'updated_at');
         $this->createDateFilters($model, 'created_at', 'updated_at');
 
 
         $q = $model::query();
         $q = $model::query();
-        $q->withoutGlobalScopes();
+        $q->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
 
 
         $this->acceptFilters($q, $request);
         $this->acceptFilters($q, $request);
         $this->acceptSearch($q, $request);
         $this->acceptSearch($q, $request);

+ 33 - 1
app/Http/Controllers/MafOrderController.php

@@ -6,6 +6,8 @@ use App\Http\Requests\StoreMafOrderRequest;
 use App\Models\MafOrder;
 use App\Models\MafOrder;
 use App\Models\MafOrdersView;
 use App\Models\MafOrdersView;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Validation\ValidationException;
 
 
 class MafOrderController extends Controller
 class MafOrderController extends Controller
 {
 {
@@ -50,6 +52,13 @@ class MafOrderController extends Controller
         $this->applyStableSorting($q);
         $this->applyStableSorting($q);
 
 
         $this->data['maf_orders'] = $q->paginate($this->data['per_page'])->withQueryString();
         $this->data['maf_orders'] = $q->paginate($this->data['per_page'])->withQueryString();
+        $this->data['order_numbers'] = MafOrder::query()
+            ->whereNotNull('order_number')
+            ->where('order_number', '!=', '')
+            ->orderBy('order_number')
+            ->distinct()
+            ->pluck('order_number');
+
         return view('maf_orders.index', $this->data);
         return view('maf_orders.index', $this->data);
     }
     }
 
 
@@ -61,7 +70,7 @@ class MafOrderController extends Controller
 
 
     public function show(Request $request, int $maf_order)
     public function show(Request $request, int $maf_order)
     {
     {
-        $this->data['maf_order'] = MafOrder::query()->withoutGlobalScopes()->find($maf_order);
+        $this->data['maf_order'] = MafOrder::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->find($maf_order);
         $this->data['previous_url'] = $this->resolvePreviousUrl(
         $this->data['previous_url'] = $this->resolvePreviousUrl(
             $request,
             $request,
             'previous_url_maf_order',
             'previous_url_maf_order',
@@ -88,4 +97,27 @@ class MafOrderController extends Controller
         $maf_order->update(['in_stock' => $maf_order->quantity, 'status' => 'на складе']);
         $maf_order->update(['in_stock' => $maf_order->quantity, 'status' => 'на складе']);
         return redirect()->route('maf_order.show', $maf_order);
         return redirect()->route('maf_order.show', $maf_order);
     }
     }
+
+    public function setOrderInStock(Request $request)
+    {
+        $validated = $request->validate([
+            'bulk_order_number' => 'required|string',
+        ]);
+
+        $orderNumber = trim((string) $validated['bulk_order_number']);
+        $query = MafOrder::query()->where('order_number', $orderNumber);
+
+        if (!$query->exists()) {
+            throw ValidationException::withMessages([
+                'bulk_order_number' => 'Заказ не найден в выбранном году.',
+            ]);
+        }
+
+        $query->update([
+            'in_stock' => DB::raw('quantity'),
+            'status' => 'на складе',
+        ]);
+
+        return redirect()->route('maf_order.index', session('gp_maf_order'));
+    }
 }
 }

+ 1 - 1
app/Http/Controllers/OrderController.php

@@ -255,7 +255,7 @@ class OrderController extends Controller
      */
      */
     public function show(Request $request, int $order)
     public function show(Request $request, int $order)
     {
     {
-        $this->data['order'] = Order::query()->withoutGlobalScopes()->find($order);
+        $this->data['order'] = Order::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->find($order);
 
 
         if ($request->boolean('sync_year') && $this->data['order']) {
         if ($request->boolean('sync_year') && $this->data['order']) {
             $previousYear = year();
             $previousYear = year();

+ 1 - 1
app/Http/Controllers/ProductController.php

@@ -86,7 +86,7 @@ class ProductController extends Controller
             'previous_url_products',
             'previous_url_products',
             route('catalog.index', session('gp_products'))
             route('catalog.index', session('gp_products'))
         );
         );
-        $this->data['product'] = Product::query()->withoutGlobalScopes()->find($product);
+        $this->data['product'] = Product::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->find($product);
         return view('catalog.edit', $this->data);
         return view('catalog.edit', $this->data);
     }
     }
 
 

+ 16 - 2
app/Http/Controllers/ProductSKUController.php

@@ -92,7 +92,7 @@ class ProductSKUController extends Controller
 
 
     public function show(Request $request, int $product_sku)
     public function show(Request $request, int $product_sku)
     {
     {
-        $this->data['product_sku'] = ProductSKU::query()->withoutGlobalScopes()->find($product_sku);
+        $this->data['product_sku'] = ProductSKU::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->find($product_sku);
         $this->data['previous_url'] = $this->resolvePreviousUrl(
         $this->data['previous_url'] = $this->resolvePreviousUrl(
             $request,
             $request,
             'previous_url_product_sku',
             'previous_url_product_sku',
@@ -145,7 +145,21 @@ class ProductSKUController extends Controller
 
 
     public function exportMaf(Request $request)
     public function exportMaf(Request $request)
     {
     {
-        $filters['year'] = year();
+        $request->validate([
+            'withFilter' => 'nullable',
+            'filters' => 'nullable|array',
+            's' => 'nullable|string',
+        ]);
+
+        $filters = [];
+        if ($request->boolean('withFilter')) {
+            $filters = $request->filters ?? [];
+            if ($request->filled('s')) {
+                $filters['s'] = $request->string('s')->toString();
+            }
+        }
+
+        $filters['year'] = (int) $request->session()->get('year', date('Y'));
         ExportMafJob::dispatch($request->user()->id, $filters);
         ExportMafJob::dispatch($request->user()->id, $filters);
         return redirect()->route('product_sku.index', session('gp_product_sku'))->with(['success' => 'Задача экспорта успешно создана!']);
         return redirect()->route('product_sku.index', session('gp_product_sku'))->with(['success' => 'Задача экспорта успешно создана!']);
     }
     }

+ 45 - 1
app/Http/Controllers/ReclamationController.php

@@ -78,6 +78,10 @@ class ReclamationController extends Controller
         $this->acceptSearch($q, $request);
         $this->acceptSearch($q, $request);
         $this->setSortAndOrderBy($model, $request);
         $this->setSortAndOrderBy($model, $request);
 
 
+        if (hasRole(Role::BRIGADIER)) {
+            $q->where('brigadier_id', auth()->id());
+        }
+
         $this->applyStableSorting($q);
         $this->applyStableSorting($q);
         $this->data['reclamations'] = $q->paginate($this->data['per_page'])->withQueryString();
         $this->data['reclamations'] = $q->paginate($this->data['per_page'])->withQueryString();
 
 
@@ -123,6 +127,8 @@ class ReclamationController extends Controller
 
 
     public function show(Request $request, Reclamation $reclamation)
     public function show(Request $request, Reclamation $reclamation)
     {
     {
+        $this->ensureCanViewReclamation($reclamation);
+
         $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id');
         $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id');
         $this->data['reclamation'] = $reclamation;
         $this->data['reclamation'] = $reclamation;
         $this->data['previous_url'] = $this->resolvePreviousUrl(
         $this->data['previous_url'] = $this->resolvePreviousUrl(
@@ -171,6 +177,9 @@ class ReclamationController extends Controller
 
 
     public function uploadPhotoBefore(Request $request, Reclamation $reclamation, FileService $fileService)
     public function uploadPhotoBefore(Request $request, Reclamation $reclamation, FileService $fileService)
     {
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
         $data = $request->validate([
             'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
             'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
         ]);
         ]);
@@ -193,6 +202,8 @@ class ReclamationController extends Controller
 
 
     public function uploadPhotoAfter(Request $request, Reclamation $reclamation, FileService $fileService)
     public function uploadPhotoAfter(Request $request, Reclamation $reclamation, FileService $fileService)
     {
     {
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
         $data = $request->validate([
             'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
             'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
         ]);
         ]);
@@ -215,6 +226,9 @@ class ReclamationController extends Controller
 
 
     public function deletePhotoBefore(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
     public function deletePhotoBefore(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
     {
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->photos_before()->detach($file);
         $reclamation->photos_before()->detach($file);
         Storage::disk('public')->delete($file->path);
         Storage::disk('public')->delete($file->path);
         $file->delete();
         $file->delete();
@@ -223,6 +237,9 @@ class ReclamationController extends Controller
 
 
     public function deletePhotoAfter(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
     public function deletePhotoAfter(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
     {
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->photos_after()->detach($file);
         $reclamation->photos_after()->detach($file);
         Storage::disk('public')->delete($file->path);
         Storage::disk('public')->delete($file->path);
         $file->delete();
         $file->delete();
@@ -231,6 +248,9 @@ class ReclamationController extends Controller
 
 
     public function uploadDocument(Request $request, Reclamation $reclamation, FileService $fileService)
     public function uploadDocument(Request $request, Reclamation $reclamation, FileService $fileService)
     {
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
         $data = $request->validate([
             'document.*' => 'file',
             'document.*' => 'file',
         ]);
         ]);
@@ -255,6 +275,9 @@ class ReclamationController extends Controller
 
 
     public function deleteDocument(Request $request, Reclamation $reclamation, File $file)
     public function deleteDocument(Request $request, Reclamation $reclamation, File $file)
     {
     {
+        $this->ensureHasRole([Role::ADMIN]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->documents()->detach($file);
         $reclamation->documents()->detach($file);
         Storage::disk('public')->delete($file->path);
         Storage::disk('public')->delete($file->path);
         $file->delete();
         $file->delete();
@@ -263,6 +286,9 @@ class ReclamationController extends Controller
 
 
     public function uploadAct(Request $request, Reclamation $reclamation, FileService $fileService)
     public function uploadAct(Request $request, Reclamation $reclamation, FileService $fileService)
     {
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER, Role::BRIGADIER, Role::WAREHOUSE_HEAD]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
         $data = $request->validate([
             'acts.*' => 'file',
             'acts.*' => 'file',
         ]);
         ]);
@@ -287,6 +313,9 @@ class ReclamationController extends Controller
 
 
     public function deleteAct(Request $request, Reclamation $reclamation, File $file)
     public function deleteAct(Request $request, Reclamation $reclamation, File $file)
     {
     {
+        $this->ensureHasRole([Role::ADMIN]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->acts()->detach($file);
         $reclamation->acts()->detach($file);
         Storage::disk('public')->delete($file->path);
         Storage::disk('public')->delete($file->path);
         $file->delete();
         $file->delete();
@@ -504,6 +533,21 @@ class ReclamationController extends Controller
     {
     {
         GenerateFilesPack::dispatch($reclamation, $reclamation->photos_after, auth()->user()->id, 'Фото после');
         GenerateFilesPack::dispatch($reclamation, $reclamation->photos_after, auth()->user()->id, 'Фото после');
         return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
         return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
-            ->with(['success' => 'Задача архивации создана!']);    }
+            ->with(['success' => 'Задача архивации создана!']);
+    }
+
+    private function ensureCanViewReclamation(Reclamation $reclamation): void
+    {
+        if (hasRole(Role::BRIGADIER) && (int)$reclamation->brigadier_id !== (int)auth()->id()) {
+            abort(403);
+        }
+    }
+
+    private function ensureHasRole(array $roles): void
+    {
+        if (!count(array_intersect($roles, Role::effectiveRoles((string)auth()->user()?->role)))) {
+            abort(403);
+        }
+    }
 
 
 }
 }

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

@@ -374,7 +374,7 @@ class ScheduleController extends Controller
 
 
         if ($orderIds) {
         if ($orderIds) {
             $orderStatuses = Order::query()
             $orderStatuses = Order::query()
-                ->withoutGlobalScopes()
+                ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->whereIn('id', array_unique($orderIds))
                 ->whereIn('id', array_unique($orderIds))
                 ->pluck('order_status_id', 'id')
                 ->pluck('order_status_id', 'id')
                 ->all();
                 ->all();
@@ -396,7 +396,7 @@ class ScheduleController extends Controller
 
 
         if ($reclamationScheduleIds) {
         if ($reclamationScheduleIds) {
             $reclamationStatuses = Reclamation::query()
             $reclamationStatuses = Reclamation::query()
-                ->withoutGlobalScopes()
+                ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->whereIn('id', array_keys($reclamationScheduleIds))
                 ->whereIn('id', array_keys($reclamationScheduleIds))
                 ->pluck('status_id', 'id')
                 ->pluck('status_id', 'id')
                 ->all();
                 ->all();

+ 9 - 9
app/Http/Controllers/YearDataController.php

@@ -104,17 +104,17 @@ class YearDataController extends Controller
     {
     {
         $years = [];
         $years = [];
 
 
-        $orderYears = Order::withoutGlobalScopes()->withTrashed()
+        $orderYears = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->selectRaw('DISTINCT year')
             ->selectRaw('DISTINCT year')
             ->pluck('year')
             ->pluck('year')
             ->toArray();
             ->toArray();
 
 
-        $productYears = Product::withoutGlobalScopes()->withTrashed()
+        $productYears = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->selectRaw('DISTINCT year')
             ->selectRaw('DISTINCT year')
             ->pluck('year')
             ->pluck('year')
             ->toArray();
             ->toArray();
 
 
-        $mafOrderYears = MafOrder::withoutGlobalScopes()->withTrashed()
+        $mafOrderYears = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->selectRaw('DISTINCT year')
             ->selectRaw('DISTINCT year')
             ->pluck('year')
             ->pluck('year')
             ->toArray();
             ->toArray();
@@ -144,10 +144,10 @@ class YearDataController extends Controller
 
 
     private function collectStats(int $year): array
     private function collectStats(int $year): array
     {
     {
-        $orderIds = Order::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $mafOrderIds = MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
-        $productSkuIds = ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $year)->pluck('id');
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $mafOrderIds = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
+        $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $year)->pluck('id');
 
 
         $reclamationCount = Reclamation::whereIn('order_id', $orderIds)->count();
         $reclamationCount = Reclamation::whereIn('order_id', $orderIds)->count();
         $fileIds = $this->collectFileIds($year, $orderIds, $productIds, $productSkuIds);
         $fileIds = $this->collectFileIds($year, $orderIds, $productIds, $productSkuIds);
@@ -195,14 +195,14 @@ class YearDataController extends Controller
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            Product::withoutGlobalScopes()->withTrashed()
+            Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productIds)
                 ->whereIn('id', $productIds)
                 ->whereNotNull('certificate_id')
                 ->whereNotNull('certificate_id')
                 ->pluck('certificate_id')
                 ->pluck('certificate_id')
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            ProductSKU::withoutGlobalScopes()->withTrashed()
+            ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productSkuIds)
                 ->whereIn('id', $productSkuIds)
                 ->whereNotNull('passport_id')
                 ->whereNotNull('passport_id')
                 ->pluck('passport_id')
                 ->pluck('passport_id')

+ 2 - 1
app/Http/Middleware/EnsureUserHasRole.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Http\Middleware;
 namespace App\Http\Middleware;
 
 
+use App\Models\Role;
 use Closure;
 use Closure;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpFoundation\Response;
@@ -18,7 +19,7 @@ class EnsureUserHasRole
      */
      */
     public function handle(Request $request, Closure $next, ... $roles): Response
     public function handle(Request $request, Closure $next, ... $roles): Response
     {
     {
-        if(in_array($request->user()->role, $roles)) {
+        if (count(array_intersect($roles, Role::effectiveRoles($request->user()->role))) > 0) {
             return $next($request);
             return $next($request);
         }
         }
         abort(403);
         abort(403);

+ 1 - 2
app/Http/Requests/StoreReclamationSparePartsRequest.php

@@ -2,7 +2,6 @@
 
 
 namespace App\Http\Requests;
 namespace App\Http\Requests;
 
 
-use App\Models\Role;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Foundation\Http\FormRequest;
 
 
 class StoreReclamationSparePartsRequest extends FormRequest
 class StoreReclamationSparePartsRequest extends FormRequest
@@ -12,7 +11,7 @@ class StoreReclamationSparePartsRequest extends FormRequest
      */
      */
     public function authorize(): bool
     public function authorize(): bool
     {
     {
-        return in_array(auth()->user()?->role, [Role::ADMIN, Role::MANAGER]);
+        return hasRole('admin,manager');
     }
     }
 
 
     /**
     /**

+ 10 - 10
app/Jobs/ClearYearDataJob.php

@@ -59,9 +59,9 @@ class ClearYearDataJob implements ShouldQueue
 
 
     private function clearData(): void
     private function clearData(): void
     {
     {
-        $orderIds = Order::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
-        $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
-        $productSkuIds = ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
+        $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
+        $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
         $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
         $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
 
 
         $fileIds = $this->collectFileIds($orderIds, $productIds, $productSkuIds);
         $fileIds = $this->collectFileIds($orderIds, $productIds, $productSkuIds);
@@ -84,19 +84,19 @@ class ClearYearDataJob implements ShouldQueue
         Schedule::whereIn('order_id', $orderIds)->delete();
         Schedule::whereIn('order_id', $orderIds)->delete();
 
 
         // SKU продуктов
         // SKU продуктов
-        ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // Заказы
         // Заказы
-        Order::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // Заказы МАФ
         // Заказы МАФ
-        MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // Продукты
         // Продукты
-        Product::withoutGlobalScopes()->withTrashed()
+        Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->whereIn('id', $productIds)
             ->whereIn('id', $productIds)
             ->update(['certificate_id' => null]);
             ->update(['certificate_id' => null]);
-        Product::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // ТТН
         // ТТН
         Ttn::where('year', $this->year)->update(['file_id' => null]);
         Ttn::where('year', $this->year)->update(['file_id' => null]);
@@ -139,14 +139,14 @@ class ClearYearDataJob implements ShouldQueue
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            Product::withoutGlobalScopes()->withTrashed()
+            Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productIds)
                 ->whereIn('id', $productIds)
                 ->whereNotNull('certificate_id')
                 ->whereNotNull('certificate_id')
                 ->pluck('certificate_id')
                 ->pluck('certificate_id')
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            ProductSKU::withoutGlobalScopes()->withTrashed()
+            ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productSkuIds)
                 ->whereIn('id', $productSkuIds)
                 ->whereNotNull('passport_id')
                 ->whereNotNull('passport_id')
                 ->pluck('passport_id')
                 ->pluck('passport_id')

+ 1 - 1
app/Jobs/ExportMafJob.php

@@ -29,7 +29,7 @@ class ExportMafJob implements ShouldQueue
     public function handle(): void
     public function handle(): void
     {
     {
         try {
         try {
-            $file = (new ExportMafService())->handle($this->userId, $this->filters['year'] ?? null);
+            $file = (new ExportMafService())->handle($this->userId, $this->filters);
             Log::info('ExportMaf job done!');
             Log::info('ExportMaf job done!');
             Log::info($file);
             Log::info($file);
             event(new SendWebSocketMessageEvent('Экспорт завершён!', $this->userId, ['download' => $file]));
             event(new SendWebSocketMessageEvent('Экспорт завершён!', $this->userId, ['download' => $file]));

+ 1 - 1
app/Jobs/NotifyOrderInScheduleJob.php

@@ -34,7 +34,7 @@ class NotifyOrderInScheduleJob implements ShouldQueue
         }
         }
 
 
         if(!is_null($this->schedule->order_id)) {
         if(!is_null($this->schedule->order_id)) {
-            $order = Order::query()->withoutGlobalScopes()->where('id', $this->schedule->order_id)->first();
+            $order = Order::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->where('id', $this->schedule->order_id)->first();
             if(!is_null($order?->user?->token_fcm)) {
             if(!is_null($order?->user?->token_fcm)) {
                 $order->user->notify(new FireBaseNotification($title, $body));
                 $order->user->notify(new FireBaseNotification($title, $body));
             }
             }

+ 2 - 2
app/Models/MafOrder.php

@@ -43,12 +43,12 @@ class MafOrder extends Model
 
 
     public function product(): BelongsTo
     public function product(): BelongsTo
     {
     {
-        return $this->belongsTo(Product::class)->withoutGlobalScopes();
+        return $this->belongsTo(Product::class)->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 
 
     public function products_sku(): HasMany
     public function products_sku(): HasMany
     {
     {
-        return $this->hasMany(ProductSKU::class)->withoutGlobalScopes();
+        return $this->hasMany(ProductSKU::class)->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 
 
 }
 }

+ 1 - 1
app/Models/MafView.php

@@ -46,6 +46,6 @@ class MafView extends Model
      */
      */
     public function product(): BelongsTo
     public function product(): BelongsTo
     {
     {
-        return $this->belongsTo(Product::class, 'product_id', 'id')->withoutGlobalScopes();
+        return $this->belongsTo(Product::class, 'product_id', 'id')->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 }
 }

+ 3 - 3
app/Models/ProductSKU.php

@@ -48,7 +48,7 @@ class ProductSKU extends Model
      */
      */
     public function product(): BelongsTo
     public function product(): BelongsTo
     {
     {
-        return $this->belongsTo(Product::class, 'product_id', 'id')->withoutGlobalScopes();
+        return $this->belongsTo(Product::class, 'product_id', 'id')->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 
 
     /**
     /**
@@ -56,7 +56,7 @@ class ProductSKU extends Model
      */
      */
     public function order(): BelongsTo
     public function order(): BelongsTo
     {
     {
-        return $this->belongsTo(Order::class, 'order_id', 'id')->withoutGlobalScopes();
+        return $this->belongsTo(Order::class, 'order_id', 'id')->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 
 
     /**
     /**
@@ -72,6 +72,6 @@ class ProductSKU extends Model
      */
      */
     public function maf_order(): BelongsTo
     public function maf_order(): BelongsTo
     {
     {
-        return $this->belongsTo(MafOrder::class, 'maf_order_id', 'id')->withoutGlobalScopes();
+        return $this->belongsTo(MafOrder::class, 'maf_order_id', 'id')->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 }
 }

+ 2 - 2
app/Models/Reclamation.php

@@ -57,7 +57,7 @@ class Reclamation extends Model
 
 
     public function order(): BelongsTo
     public function order(): BelongsTo
     {
     {
-        return $this->belongsTo(Order::class)->withoutGlobalScopes();
+        return $this->belongsTo(Order::class)->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 
 
     public function status(): BelongsTo
     public function status(): BelongsTo
@@ -72,7 +72,7 @@ class Reclamation extends Model
             'reclamation_product_sku',
             'reclamation_product_sku',
             'reclamation_id',
             'reclamation_id',
             'product_sku_id')
             'product_sku_id')
-            ->withoutGlobalScopes();
+            ->withoutGlobalScope(\App\Models\Scopes\YearScope::class);
     }
     }
 
 
     public function user(): BelongsTo
     public function user(): BelongsTo

+ 19 - 4
app/Models/Role.php

@@ -4,18 +4,33 @@ namespace App\Models;
 
 
 class Role
 class Role
 {
 {
-    const ADMIN     = 'admin';
-    const MANAGER   = 'manager';
+    const ADMIN = 'admin';
+    const MANAGER = 'manager';
     const BRIGADIER = 'brigadier';
     const BRIGADIER = 'brigadier';
+    const WAREHOUSE_HEAD = 'warehouse_head';
+    const ASSISTANT_HEAD = 'assistant_head';
+
     const VALID_ROLES = [
     const VALID_ROLES = [
         self::ADMIN,
         self::ADMIN,
         self::MANAGER,
         self::MANAGER,
         self::BRIGADIER,
         self::BRIGADIER,
+        self::WAREHOUSE_HEAD,
+        self::ASSISTANT_HEAD,
     ];
     ];
 
 
     const NAMES = [
     const NAMES = [
-        self::ADMIN     => 'Админ',
-        self::MANAGER   => 'Менеджер',
+        self::ADMIN => 'Админ',
+        self::MANAGER => 'Менеджер',
         self::BRIGADIER => 'Бригадир',
         self::BRIGADIER => 'Бригадир',
+        self::WAREHOUSE_HEAD => 'Рук. Склада',
+        self::ASSISTANT_HEAD => 'Помощник рук.',
     ];
     ];
+
+    public static function effectiveRoles(string $role): array
+    {
+        return match ($role) {
+            self::ASSISTANT_HEAD => [self::ASSISTANT_HEAD, self::ADMIN, self::MANAGER],
+            default => [$role],
+        };
+    }
 }
 }

+ 7 - 7
app/Services/Export/ExportYearDataService.php

@@ -97,7 +97,7 @@ class ExportYearDataService
 
 
     private function exportProducts(): void
     private function exportProducts(): void
     {
     {
-        $products = Product::withoutGlobalScopes()
+        $products = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->with('certificate')
             ->with('certificate')
@@ -169,7 +169,7 @@ class ExportYearDataService
 
 
     private function exportMafOrders(): void
     private function exportMafOrders(): void
     {
     {
-        $mafOrders = MafOrder::withoutGlobalScopes()
+        $mafOrders = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->with(['user', 'product'])
             ->with(['user', 'product'])
@@ -215,7 +215,7 @@ class ExportYearDataService
 
 
     private function exportOrders(): void
     private function exportOrders(): void
     {
     {
-        $orders = Order::withoutGlobalScopes()
+        $orders = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->with(['user', 'district', 'area', 'objectType', 'brigadier', 'orderStatus'])
             ->with(['user', 'district', 'area', 'objectType', 'brigadier', 'orderStatus'])
@@ -277,7 +277,7 @@ class ExportYearDataService
 
 
     private function exportProductsSku(): void
     private function exportProductsSku(): void
     {
     {
-        $skus = ProductSKU::withoutGlobalScopes()
+        $skus = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->with(['product', 'order', 'maf_order', 'passport'])
             ->with(['product', 'order', 'maf_order', 'passport'])
@@ -337,7 +337,7 @@ class ExportYearDataService
 
 
     private function exportReclamations(): void
     private function exportReclamations(): void
     {
     {
-        $orderIds = Order::withoutGlobalScopes()
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->pluck('id');
             ->pluck('id');
@@ -440,7 +440,7 @@ class ExportYearDataService
 
 
     private function exportSchedules(): void
     private function exportSchedules(): void
     {
     {
-        $orderIds = Order::withoutGlobalScopes()
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->pluck('id');
             ->pluck('id');
@@ -566,7 +566,7 @@ class ExportYearDataService
 
 
     private function copyFilesAndExportPivots(): void
     private function copyFilesAndExportPivots(): void
     {
     {
-        $orderIds = Order::withoutGlobalScopes()
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->withTrashed()
             ->withTrashed()
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->pluck('id');
             ->pluck('id');

+ 80 - 6
app/Services/ExportMafService.php

@@ -5,7 +5,10 @@ namespace App\Services;
 use App\Helpers\DateHelper;
 use App\Helpers\DateHelper;
 use App\Models\File;
 use App\Models\File;
 use App\Models\MafView;
 use App\Models\MafView;
+use App\Models\Scopes\YearScope;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
 use PhpOffice\PhpSpreadsheet\IOFactory;
 use PhpOffice\PhpSpreadsheet\IOFactory;
 use PhpOffice\PhpSpreadsheet\Style\Border;
 use PhpOffice\PhpSpreadsheet\Style\Border;
 use PhpOffice\PhpSpreadsheet\Style\Color;
 use PhpOffice\PhpSpreadsheet\Style\Color;
@@ -15,8 +18,11 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 
 
 class ExportMafService
 class ExportMafService
 {
 {
-    public function handle(int $userId, ?int $year = null): string
+    public function handle(int $userId, array|int $filters = []): string
     {
     {
+        if (is_int($filters)) {
+            $filters = ['year' => $filters];
+        }
 
 
         $inputFileType = 'Xlsx'; // Xlsx - Xml - Ods - Slk - Gnumeric - Csv
         $inputFileType = 'Xlsx'; // Xlsx - Xml - Ods - Slk - Gnumeric - Csv
         $inputFileName = './templates/Mafs.xlsx';
         $inputFileName = './templates/Mafs.xlsx';
@@ -26,12 +32,13 @@ class ExportMafService
         $sheet = $spreadsheet->getActiveSheet();
         $sheet = $spreadsheet->getActiveSheet();
         $sheet->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
         $sheet->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
 
 
+        $year = (int) ($filters['year'] ?? date('Y'));
+        $query = MafView::query()
+            ->withoutGlobalScope(YearScope::class)
+            ->where('year', $year);
 
 
-        $targetYear = $year ?? year();
-        $mafs = MafView::query()
-            ->withoutGlobalScopes()
-            ->where('year', $targetYear)
-            ->get();
+        $this->applyFilters($query, $filters);
+        $mafs = $query->get();
         $i = 2;
         $i = 2;
         foreach ($mafs as $maf) {
         foreach ($mafs as $maf) {
             $sheet->setCellValue('A' . $i, $maf->id);
             $sheet->setCellValue('A' . $i, $maf->id);
@@ -86,4 +93,71 @@ class ExportMafService
         return $fileName;
         return $fileName;
 
 
     }
     }
+
+    private function applyFilters(Builder $query, array $filters): void
+    {
+        $allowedColumns = array_flip((new MafView())->getFillable());
+
+        foreach ($filters as $filterName => $filterValue) {
+            if ($filterName === 'year' || $filterValue === null || $filterValue === '') {
+                continue;
+            }
+
+            if ($filterName === 's') {
+                $searchFields = [
+                    'rfid',
+                    'factory_number',
+                    'statement_number',
+                    'upd_number',
+                    'object_address',
+                    'nomenclature_number',
+                    'article',
+                ];
+                $query->where(function ($subQuery) use ($searchFields, $filterValue) {
+                    foreach ($searchFields as $searchField) {
+                        $subQuery->orWhere($searchField, 'LIKE', '%' . $filterValue . '%');
+                    }
+                });
+                continue;
+            }
+
+            if (Str::endsWith($filterName, '_from') || Str::endsWith($filterName, '_to')) {
+                $operator = Str::endsWith($filterName, '_from') ? '>=' : '<=';
+                $column = Str::replace(['_from', '_to'], '', $filterName);
+                if (!isset($allowedColumns[$column])) {
+                    continue;
+                }
+                $query->where($column, $operator, $filterValue);
+                continue;
+            }
+
+            if (!isset($allowedColumns[$filterName])) {
+                continue;
+            }
+
+            if (is_string($filterValue) && Str::contains($filterValue, '||')) {
+                $values = explode('||', $filterValue);
+                $query->where(function ($subQuery) use ($filterName, $values) {
+                    $nonNullValues = [];
+                    foreach ($values as $value) {
+                        if ($value === '-пусто-') {
+                            $subQuery->orWhereNull($filterName);
+                        } else {
+                            $nonNullValues[] = $value;
+                        }
+                    }
+                    if (!empty($nonNullValues)) {
+                        $subQuery->orWhereIn($filterName, $nonNullValues);
+                    }
+                });
+                continue;
+            }
+
+            if ($filterValue === '-пусто-') {
+                $query->whereNull($filterName);
+            } else {
+                $query->where($filterName, $filterValue);
+            }
+        }
+    }
 }
 }

+ 2 - 1
app/Services/ExportOneOrderService.php

@@ -5,6 +5,7 @@ namespace App\Services;
 use App\Helpers\DateHelper;
 use App\Helpers\DateHelper;
 use App\Models\Order;
 use App\Models\Order;
 use App\Models\Product;
 use App\Models\Product;
+use App\Models\Scopes\YearScope;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\Storage;
@@ -27,7 +28,7 @@ class ExportOneOrderService
     public function handle(Order $order, int $userId): string
     public function handle(Order $order, int $userId): string
     {
     {
         $productsSku = $order->products_sku()
         $productsSku = $order->products_sku()
-            ->withoutGlobalScopes()
+            ->withoutGlobalScope(YearScope::class)
             ->where('year', $order->year)
             ->where('year', $order->year)
             ->get();
             ->get();
         $order->setRelation('products_sku', $productsSku);
         $order->setRelation('products_sku', $productsSku);

+ 1 - 1
app/Services/ExportOrdersService.php

@@ -32,7 +32,7 @@ class ExportOrdersService
         $productsSkuByOrder = collect();
         $productsSkuByOrder = collect();
         if ($orderIds->isNotEmpty() && $orderYears->isNotEmpty()) {
         if ($orderIds->isNotEmpty() && $orderYears->isNotEmpty()) {
             $productsSkuByOrder = ProductSKU::query()
             $productsSkuByOrder = ProductSKU::query()
-                ->withoutGlobalScopes()
+                ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->whereIn('order_id', $orderIds)
                 ->whereIn('order_id', $orderIds)
                 ->whereIn('year', $orderYears)
                 ->whereIn('year', $orderYears)
                 ->get()
                 ->get()

+ 2 - 2
app/Services/ExportReclamationsService.php

@@ -36,7 +36,7 @@ class ExportReclamationsService
             ->with([
             ->with([
                 'order.district',
                 'order.district',
                 'order.area',
                 'order.area',
-                'skus.product' => fn($q) => $q->withoutGlobalScopes(),
+                'skus.product' => fn($q) => $q->withoutGlobalScope(\App\Models\Scopes\YearScope::class),
                 'spareParts.pricingCodes',
                 'spareParts.pricingCodes',
             ])
             ])
             ->get()
             ->get()
@@ -163,7 +163,7 @@ class ExportReclamationsService
 
 
         $passportName = null;
         $passportName = null;
         if ($product->article && $year) {
         if ($product->article && $year) {
-            $productForYear = Product::withoutGlobalScopes()
+            $productForYear = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->where('year', $year)
                 ->where('year', $year)
                 ->where('article', $product->article)
                 ->where('article', $product->article)
                 ->first();
                 ->first();

+ 4 - 4
app/Services/GenerateDocumentsService.php

@@ -37,14 +37,14 @@ class GenerateDocumentsService
     {
     {
         $techDocsPath = base_path('/tech-docs/');
         $techDocsPath = base_path('/tech-docs/');
         $order = Order::query()
         $order = Order::query()
-            ->withoutGlobalScopes()
+            ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->whereKey($order->id)
             ->whereKey($order->id)
             ->where('year', $order->year)
             ->where('year', $order->year)
             ->with('documents')
             ->with('documents')
             ->firstOrFail();
             ->firstOrFail();
 
 
         $products_sku = $order->products_sku()
         $products_sku = $order->products_sku()
-            ->withoutGlobalScopes()
+            ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->where('year', $order->year)
             ->where('year', $order->year)
             ->get();
             ->get();
         $order->setRelation('products_sku', $products_sku);
         $order->setRelation('products_sku', $products_sku);
@@ -137,7 +137,7 @@ class GenerateDocumentsService
     public function generateHandoverPack(Order $order, int $userId): string
     public function generateHandoverPack(Order $order, int $userId): string
     {
     {
         $productsSku = $order->products_sku()
         $productsSku = $order->products_sku()
-            ->withoutGlobalScopes()
+            ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->where('year', $order->year)
             ->where('year', $order->year)
             ->get();
             ->get();
         $order->setRelation('products_sku', $productsSku);
         $order->setRelation('products_sku', $productsSku);
@@ -699,7 +699,7 @@ class GenerateDocumentsService
 
 
     public function generateTtnPack(Ttn $ttn, int $userId): string
     public function generateTtnPack(Ttn $ttn, int $userId): string
     {
     {
-        $skus = ProductSKU::query()->withoutGlobalScopes()->whereIn('id', json_decode($ttn->skus))->get();
+        $skus = ProductSKU::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->whereIn('id', json_decode($ttn->skus))->get();
         $volume = $weight = $places = 0;
         $volume = $weight = $places = 0;
         foreach ($skus as $sku) {
         foreach ($skus as $sku) {
             if(!isset($order)) {
             if(!isset($order)) {

+ 1 - 1
app/Services/Import/ImportMafOrdersService.php

@@ -122,7 +122,7 @@ class ImportMafOrdersService
 
 
     private function loadProductCache(): void
     private function loadProductCache(): void
     {
     {
-        $this->productCache = Product::withoutGlobalScopes()
+        $this->productCache = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
             ->where('year', $this->year)
             ->where('year', $this->year)
             ->pluck('id', 'article')
             ->pluck('id', 'article')
             ->toArray();
             ->toArray();

+ 25 - 22
app/Services/Import/ImportYearDataService.php

@@ -207,9 +207,9 @@ class ImportYearDataService
     private function clearExistingData(): void
     private function clearExistingData(): void
     {
     {
         // Используем логику из ClearYearDataJob
         // Используем логику из ClearYearDataJob
-        $orderIds = Order::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
-        $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
-        $productSkuIds = ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
+        $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
+        $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
+        $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
         $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
         $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
 
 
         // Собираем файлы до удаления связей
         // Собираем файлы до удаления связей
@@ -233,19 +233,19 @@ class ImportYearDataService
         Schedule::whereIn('order_id', $orderIds)->delete();
         Schedule::whereIn('order_id', $orderIds)->delete();
 
 
         // SKU
         // SKU
-        ProductSKU::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // Заказы
         // Заказы
-        Order::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // МАФ заказы
         // МАФ заказы
-        MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // Продукты
         // Продукты
-        Product::withoutGlobalScopes()->withTrashed()
+        Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
             ->whereIn('id', $productIds)
             ->whereIn('id', $productIds)
             ->update(['certificate_id' => null]);
             ->update(['certificate_id' => null]);
-        Product::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
+        Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
 
 
         // ТТН
         // ТТН
         Ttn::where('year', $this->year)->update(['file_id' => null]);
         Ttn::where('year', $this->year)->update(['file_id' => null]);
@@ -276,14 +276,14 @@ class ImportYearDataService
         $fileIds = $fileIds->merge(DB::table('reclamation_act')->whereIn('reclamation_id', $reclamationIds)->pluck('file_id'));
         $fileIds = $fileIds->merge(DB::table('reclamation_act')->whereIn('reclamation_id', $reclamationIds)->pluck('file_id'));
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            Product::withoutGlobalScopes()->withTrashed()
+            Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productIds)
                 ->whereIn('id', $productIds)
                 ->whereNotNull('certificate_id')
                 ->whereNotNull('certificate_id')
                 ->pluck('certificate_id')
                 ->pluck('certificate_id')
         );
         );
 
 
         $fileIds = $fileIds->merge(
         $fileIds = $fileIds->merge(
-            ProductSKU::withoutGlobalScopes()->withTrashed()
+            ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
                 ->whereIn('id', $productSkuIds)
                 ->whereIn('id', $productSkuIds)
                 ->whereNotNull('passport_id')
                 ->whereNotNull('passport_id')
                 ->pluck('passport_id')
                 ->pluck('passport_id')
@@ -319,6 +319,9 @@ class ImportYearDataService
             return $default;
             return $default;
         }
         }
         $value = $row[$headerMap[$key]] ?? null;
         $value = $row[$headerMap[$key]] ?? null;
+        if (is_string($value)) {
+            $value = trim($value);
+        }
         if ($value === null || $value === '') {
         if ($value === null || $value === '') {
             return $default;
             return $default;
         }
         }
@@ -373,7 +376,7 @@ class ImportYearDataService
             $nomenclatureNumber = $this->getStringValue($row, $headerMap, 'nomenclature_number', '');
             $nomenclatureNumber = $this->getStringValue($row, $headerMap, 'nomenclature_number', '');
 
 
             // Проверяем существует ли продукт
             // Проверяем существует ли продукт
-            $existing = Product::withoutGlobalScopes()
+            $existing = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->where('year', $this->year)
                 ->where('year', $this->year)
                 ->where('nomenclature_number', $nomenclatureNumber)
                 ->where('nomenclature_number', $nomenclatureNumber)
                 ->first();
                 ->first();
@@ -417,7 +420,7 @@ class ImportYearDataService
                 $existing->update($productData);
                 $existing->update($productData);
                 $this->productIdMapping[$oldId] = $existing->id;
                 $this->productIdMapping[$oldId] = $existing->id;
             } else {
             } else {
-                $product = Product::withoutGlobalScopes()->create($productData);
+                $product = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($productData);
                 $this->productIdMapping[$oldId] = $product->id;
                 $this->productIdMapping[$oldId] = $product->id;
             }
             }
 
 
@@ -460,7 +463,7 @@ class ImportYearDataService
                 // Пробуем найти по номенклатуре
                 // Пробуем найти по номенклатуре
                 $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
                 $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
                 if ($nomenclature) {
                 if ($nomenclature) {
-                    $product = Product::withoutGlobalScopes()
+                    $product = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                         ->where('year', $this->year)
                         ->where('year', $this->year)
                         ->where('nomenclature_number', $nomenclature)
                         ->where('nomenclature_number', $nomenclature)
                         ->first();
                         ->first();
@@ -474,7 +477,7 @@ class ImportYearDataService
             }
             }
 
 
             // Проверяем существует ли
             // Проверяем существует ли
-            $existing = MafOrder::withoutGlobalScopes()
+            $existing = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->where('year', $this->year)
                 ->where('year', $this->year)
                 ->where('product_id', $newProductId)
                 ->where('product_id', $newProductId)
                 ->where('order_number', $orderNumber)
                 ->where('order_number', $orderNumber)
@@ -498,7 +501,7 @@ class ImportYearDataService
                 $existing->update($mafOrderData);
                 $existing->update($mafOrderData);
                 $this->mafOrderIdMapping[$oldId] = $existing->id;
                 $this->mafOrderIdMapping[$oldId] = $existing->id;
             } else {
             } else {
-                $mafOrder = MafOrder::withoutGlobalScopes()->create($mafOrderData);
+                $mafOrder = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($mafOrderData);
                 $this->mafOrderIdMapping[$oldId] = $mafOrder->id;
                 $this->mafOrderIdMapping[$oldId] = $mafOrder->id;
             }
             }
 
 
@@ -535,7 +538,7 @@ class ImportYearDataService
             $objectAddress = $this->getStringValue($row, $headerMap, 'object_address', '');
             $objectAddress = $this->getStringValue($row, $headerMap, 'object_address', '');
 
 
             // Проверяем существует ли
             // Проверяем существует ли
-            $existing = Order::withoutGlobalScopes()
+            $existing = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                 ->where('year', $this->year)
                 ->where('year', $this->year)
                 ->where('object_address', $objectAddress)
                 ->where('object_address', $objectAddress)
                 ->first();
                 ->first();
@@ -582,7 +585,7 @@ class ImportYearDataService
                 $existing->update($orderData);
                 $existing->update($orderData);
                 $this->orderIdMapping[$oldId] = $existing->id;
                 $this->orderIdMapping[$oldId] = $existing->id;
             } else {
             } else {
-                $order = Order::withoutGlobalScopes()->create($orderData);
+                $order = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($orderData);
                 $this->orderIdMapping[$oldId] = $order->id;
                 $this->orderIdMapping[$oldId] = $order->id;
             }
             }
 
 
@@ -625,7 +628,7 @@ class ImportYearDataService
                 // Пробуем найти по номенклатуре
                 // Пробуем найти по номенклатуре
                 $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
                 $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
                 if ($nomenclature) {
                 if ($nomenclature) {
-                    $product = Product::withoutGlobalScopes()
+                    $product = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                         ->where('year', $this->year)
                         ->where('year', $this->year)
                         ->where('nomenclature_number', $nomenclature)
                         ->where('nomenclature_number', $nomenclature)
                         ->first();
                         ->first();
@@ -646,7 +649,7 @@ class ImportYearDataService
                 // Пробуем найти по адресу
                 // Пробуем найти по адресу
                 $orderAddress = $this->getValue($row, $headerMap, 'order_address');
                 $orderAddress = $this->getValue($row, $headerMap, 'order_address');
                 if ($orderAddress) {
                 if ($orderAddress) {
-                    $order = Order::withoutGlobalScopes()
+                    $order = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                         ->where('year', $this->year)
                         ->where('year', $this->year)
                         ->where('object_address', $orderAddress)
                         ->where('object_address', $orderAddress)
                         ->first();
                         ->first();
@@ -669,7 +672,7 @@ class ImportYearDataService
                     // Пробуем найти по номеру заказа
                     // Пробуем найти по номеру заказа
                     $mafOrderNumber = $this->getValue($row, $headerMap, 'maf_order_number');
                     $mafOrderNumber = $this->getValue($row, $headerMap, 'maf_order_number');
                     if ($mafOrderNumber) {
                     if ($mafOrderNumber) {
-                        $mafOrder = MafOrder::withoutGlobalScopes()
+                        $mafOrder = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                             ->where('year', $this->year)
                             ->where('year', $this->year)
                             ->where('product_id', $newProductId)
                             ->where('product_id', $newProductId)
                             ->where('order_number', $mafOrderNumber)
                             ->where('order_number', $mafOrderNumber)
@@ -702,7 +705,7 @@ class ImportYearDataService
                 'passport_id' => $passportId,
                 'passport_id' => $passportId,
             ];
             ];
 
 
-            $sku = ProductSKU::withoutGlobalScopes()->create($skuData);
+            $sku = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($skuData);
             $this->productSkuIdMapping[$oldId] = $sku->id;
             $this->productSkuIdMapping[$oldId] = $sku->id;
 
 
             $count++;
             $count++;
@@ -750,7 +753,7 @@ class ImportYearDataService
                 // Пробуем найти по адресу
                 // Пробуем найти по адресу
                 $orderAddress = $this->getValue($row, $headerMap, 'order_address');
                 $orderAddress = $this->getValue($row, $headerMap, 'order_address');
                 if ($orderAddress) {
                 if ($orderAddress) {
-                    $order = Order::withoutGlobalScopes()
+                    $order = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                         ->where('year', $this->year)
                         ->where('year', $this->year)
                         ->where('object_address', $orderAddress)
                         ->where('object_address', $orderAddress)
                         ->first();
                         ->first();

+ 1 - 1
app/Services/ImportCatalogService.php

@@ -76,7 +76,7 @@ class ImportCatalogService extends ImportBaseService
                 $certDate = (int) $r['certificate_date'];
                 $certDate = (int) $r['certificate_date'];
 
 
                 $existing = Product::query()
                 $existing = Product::query()
-                    ->withoutGlobalScopes()
+                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                     ->where('year', $this->year)
                     ->where('year', $this->year)
                     ->where('nomenclature_number', $r['nomenclature_number'])
                     ->where('nomenclature_number', $r['nomenclature_number'])
                     ->first();
                     ->first();

+ 1 - 1
app/Services/ImportMafsService.php

@@ -80,7 +80,7 @@ class ImportMafsService extends ImportBaseService
                 $logMessage .= $r['products_sku.statement_date'] . ' ' . $r['products_sku.manufacture_date'] .' ';
                 $logMessage .= $r['products_sku.statement_date'] . ' ' . $r['products_sku.manufacture_date'] .' ';
 
 
                 $productSKU = ProductSKU::query()
                 $productSKU = ProductSKU::query()
-                    ->withoutGlobalScopes()
+                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                     ->where('id', $id)
                     ->where('id', $id)
                     ->when($this->year, fn ($q) => $q->where('year', $this->year))
                     ->when($this->year, fn ($q) => $q->where('year', $this->year))
                     ->first();
                     ->first();

+ 42 - 37
app/Services/ImportOrdersService.php

@@ -123,7 +123,7 @@ class ImportOrdersService extends ImportBaseService
 
 
                 // product
                 // product
                 $product = Product::query()
                 $product = Product::query()
-                    ->withoutGlobalScopes()
+                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                     ->where('year', $year)
                     ->where('year', $year)
                     ->where('nomenclature_number', $r['products.nomenclature_number'])
                     ->where('nomenclature_number', $r['products.nomenclature_number'])
                     ->first();
                     ->first();
@@ -160,7 +160,7 @@ class ImportOrdersService extends ImportBaseService
 
 
                 // order
                 // order
                 $order = Order::query()
                 $order = Order::query()
-                    ->withoutGlobalScopes()
+                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                     ->where('year', $year)
                     ->where('year', $year)
                     ->where('object_address', $r['orders.object_address'])
                     ->where('object_address', $r['orders.object_address'])
                     ->first();
                     ->first();
@@ -168,7 +168,7 @@ class ImportOrdersService extends ImportBaseService
                     $order = Order::query()
                     $order = Order::query()
                         ->create([
                         ->create([
                             'year' => $year,
                             'year' => $year,
-                            'name' => $r['orders.object_address'],
+                            'name' => $r['orders.name'],
                             'user_id' => $userId,
                             'user_id' => $userId,
                             'district_id' => $districtId,
                             'district_id' => $districtId,
                             'area_id' => $areaId,
                             'area_id' => $areaId,
@@ -188,7 +188,7 @@ class ImportOrdersService extends ImportBaseService
                 // maf order
                 // maf order
                 if ($r['maf_orders.order_number'] != '') {
                 if ($r['maf_orders.order_number'] != '') {
                     $mafOrder = MafOrder::query()
                     $mafOrder = MafOrder::query()
-                        ->withoutGlobalScopes()
+                        ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
                         ->where('year', $year)
                         ->where('year', $year)
                         ->where('product_id', $product->id)
                         ->where('product_id', $product->id)
                         ->where('order_number', $r['maf_orders.order_number'])
                         ->where('order_number', $r['maf_orders.order_number'])
@@ -219,42 +219,47 @@ class ImportOrdersService extends ImportBaseService
                 // search rfid in products_sku
                 // search rfid in products_sku
                 $manufacture_date = is_int($r['products_sku.manufacture_date']) ? DateHelper::excelDateToISODate($r['products_sku.manufacture_date']) : null;
                 $manufacture_date = is_int($r['products_sku.manufacture_date']) ? DateHelper::excelDateToISODate($r['products_sku.manufacture_date']) : null;
                 $statement_date = is_int($r['products_sku.statement_date']) ? DateHelper::excelDateToISODate($r['products_sku.statement_date']) : null;
                 $statement_date = is_int($r['products_sku.statement_date']) ? DateHelper::excelDateToISODate($r['products_sku.statement_date']) : null;
+                $rfid = trim((string) $r['products_sku.rfid']) ?: null;
 
 
-                $productSKU = ProductSKU::query()
-                    ->withoutGlobalScopes()
-                    ->where('year', $year)
-                    ->where('rfid', $r['products_sku.rfid'])
-                    ->where('product_id', $product->id)
-                    ->where('order_id', $order->id)
-                    ->where('maf_order_id', $mafOrder?->id)
-                    ->where('factory_number', $r['products_sku.factory_number'])
-                    ->where('statement_number', $r['products_sku.statement_number'])
-                    ->where('upd_number', $r['products_sku.upd_number'])
-                    ->where('statement_date', $statement_date)
-                    ->where('manufacture_date', $manufacture_date)
-                    ->first();
-//            dd($productSKU->toRawSql());
-                if ($productSKU) {
-                    echo $this->import->log($logMessage . 'Found product with rfid: ' . $productSKU->rfid . ' Skip.' . '. ');
-                    continue;
-                } else {
-                    $productSKU = ProductSKU::query()->create([
-                        'year' => $year,
-                        'product_id' => $product->id,
-                        'order_id' => $order->id,
-                        'maf_order_id' => $mafOrder?->id,
-                        'status' => ($mafOrder?->id) ? 'отгружен' : 'требуется',
-                        'rfid' => $r['products_sku.rfid'],
-                        'factory_number' => $r['products_sku.factory_number'],
-                        'manufacture_date' => $manufacture_date,
-                        'statement_number' => $r['products_sku.statement_number'],
-                        'statement_date' => $statement_date,
-                        'upd_number' => $r['products_sku.upd_number'],
-                    ]);
-                    $result['productsSkuCreated'] += 1;
-                    $logMessage .= 'Created product sku: ' . $productSKU->id . '. ';
+                // Дедупликация возможна только при заполненном RFID.
+                // Если RFID пустой, нужно создавать новую позицию, чтобы поддержать несколько одинаковых товаров на одной площадке.
+                if ($rfid) {
+                    $productSKU = ProductSKU::query()
+                        ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
+                        ->where('year', $year)
+                        ->where('rfid', $rfid)
+                        ->where('product_id', $product->id)
+                        ->where('order_id', $order->id)
+                        ->where('maf_order_id', $mafOrder?->id)
+                        ->where('factory_number', $r['products_sku.factory_number'])
+                        ->where('statement_number', $r['products_sku.statement_number'])
+                        ->where('upd_number', $r['products_sku.upd_number'])
+                        ->where('statement_date', $statement_date)
+                        ->where('manufacture_date', $manufacture_date)
+                        ->first();
+
+                    if ($productSKU) {
+                        echo $this->import->log($logMessage . 'Found product with rfid: ' . $productSKU->rfid . ' Skip.' . '. ');
+                        continue;
+                    }
                 }
                 }
 
 
+                $productSKU = ProductSKU::query()->create([
+                    'year' => $year,
+                    'product_id' => $product->id,
+                    'order_id' => $order->id,
+                    'maf_order_id' => $mafOrder?->id,
+                    'status' => ($mafOrder?->id) ? 'отгружен' : 'требуется',
+                    'rfid' => $rfid,
+                    'factory_number' => $r['products_sku.factory_number'],
+                    'manufacture_date' => $manufacture_date,
+                    'statement_number' => $r['products_sku.statement_number'],
+                    'statement_date' => $statement_date,
+                    'upd_number' => $r['products_sku.upd_number'],
+                ]);
+                $result['productsSkuCreated'] += 1;
+                $logMessage .= 'Created product sku: ' . $productSKU->id . '. ';
+
                 echo $this->import->log($logMessage);
                 echo $this->import->log($logMessage);
             } catch (\Exception $e) {
             } catch (\Exception $e) {
                 $this->errorRows[$strNumber] = $e->getMessage();
                 $this->errorRows[$strNumber] = $e->getMessage();

+ 4 - 4
app/Services/ImportReclamationsService.php

@@ -99,7 +99,7 @@ class ImportReclamationsService extends ImportBaseService
 
 
                 // order
                 // order
 //                $order = Order::query()
 //                $order = Order::query()
-//                    ->withoutGlobalScopes()
+//                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
 //                    ->where('year', $year)
 //                    ->where('year', $year)
 //                    ->where('object_address', $r['orders.object_address'])
 //                    ->where('object_address', $r['orders.object_address'])
 //                    ->first();
 //                    ->first();
@@ -113,7 +113,7 @@ class ImportReclamationsService extends ImportBaseService
 
 
                 // product
                 // product
 //                $product = Product::query()
 //                $product = Product::query()
-//                    ->withoutGlobalScopes()
+//                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
 //                    ->where('year', $year)
 //                    ->where('year', $year)
 //                    ->where('nomenclature_number', $r['products.nomenclature_number'])
 //                    ->where('nomenclature_number', $r['products.nomenclature_number'])
 //                    ->first();
 //                    ->first();
@@ -127,7 +127,7 @@ class ImportReclamationsService extends ImportBaseService
 
 
                 // check maf with this nomenclature number in order
                 // check maf with this nomenclature number in order
                 $productSKU = ProductSKU::query()
                 $productSKU = ProductSKU::query()
-                    ->withoutGlobalScopes()
+                    ->withoutGlobalScope(\App\Models\Scopes\YearScope::class)
 //                    ->where('year', $year)
 //                    ->where('year', $year)
 //                    ->where('product_id', $product->id)
 //                    ->where('product_id', $product->id)
 //                    ->where('order_id', $order->id)
 //                    ->where('order_id', $order->id)
@@ -140,7 +140,7 @@ class ImportReclamationsService extends ImportBaseService
                     continue;
                     continue;
                 }
                 }
 
 
-                $order = Order::query()->withoutGlobalScopes()->where('id', $productSKU->order_id)->first();
+                $order = Order::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->where('id', $productSKU->order_id)->first();
 
 
                 $createDate = ($r['reclamations.create_date']) ? DateHelper::excelDateToISODate($r['reclamations.create_date']) : null;
                 $createDate = ($r['reclamations.create_date']) ? DateHelper::excelDateToISODate($r['reclamations.create_date']) : null;
                 $finishDate = ($r['reclamations.finish_date']) ? DateHelper::excelDateToISODate($r['reclamations.finish_date']) : null;
                 $finishDate = ($r['reclamations.finish_date']) ? DateHelper::excelDateToISODate($r['reclamations.finish_date']) : null;

+ 14 - 0
database/factories/UserFactory.php

@@ -55,6 +55,20 @@ class UserFactory extends Factory
         ]);
         ]);
     }
     }
 
 
+    public function warehouseHead(): static
+    {
+        return $this->state(fn (array $attributes) => [
+            'role' => 'warehouse_head',
+        ]);
+    }
+
+    public function assistantHead(): static
+    {
+        return $this->state(fn (array $attributes) => [
+            'role' => 'assistant_head',
+        ]);
+    }
+
     /**
     /**
      * Indicate that the model's email address should be unverified.
      * Indicate that the model's email address should be unverified.
      */
      */

+ 32 - 0
database/migrations/2026_02_26_120000_trim_whitespace_in_order_related_fields.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        $columnsByTable = [
+            'orders' => ['name', 'object_address', 'tg_group_name', 'tg_group_link'],
+            'users' => ['name'],
+            'districts' => ['shortname', 'name'],
+            'areas' => ['name'],
+            'object_types' => ['name'],
+            'order_statuses' => ['name'],
+        ];
+
+        foreach ($columnsByTable as $table => $columns) {
+            foreach ($columns as $column) {
+                DB::statement(
+                    "UPDATE {$table} SET {$column} = TRIM({$column}) WHERE {$column} IS NOT NULL AND {$column} <> TRIM({$column})"
+                );
+            }
+        }
+    }
+
+    public function down(): void
+    {
+        // Irreversible: trimmed whitespace cannot be restored.
+    }
+};

+ 9 - 1
resources/sass/app.scss

@@ -45,6 +45,12 @@
 
 
 .js-main-table-scroll {
 .js-main-table-scroll {
   overflow: auto;
   overflow: auto;
+  position: relative;
+  z-index: 20;
+}
+
+.js-main-table-scroll:has(.dropdown-menu.show) {
+  overflow: visible;
 }
 }
 
 
 .js-main-table-scroll .table {
 .js-main-table-scroll .table {
@@ -67,7 +73,7 @@
 }
 }
 
 
 .js-main-table-scroll thead th .dropdown-menu {
 .js-main-table-scroll thead th .dropdown-menu {
-  z-index: 11;
+  z-index: 1080;
 }
 }
 
 
 .js-subtable-scroll {
 .js-subtable-scroll {
@@ -136,6 +142,8 @@
   padding-bottom: 0.25rem !important;
   padding-bottom: 0.25rem !important;
   background-color: var(--bs-body-bg);
   background-color: var(--bs-body-bg);
   box-shadow: 0 -8px 14px -10px rgba(0, 0, 0, 0.35);
   box-shadow: 0 -8px 14px -10px rgba(0, 0, 0, 0.35);
+  position: relative;
+  z-index: 1;
 }
 }
 
 
 .table-pagination-row .pagination {
 .table-pagination-row .pagination {

+ 3 - 1
resources/views/layouts/menu.blade.php

@@ -22,9 +22,11 @@
                             href="{{ route('schedule.index', session('gp_schedule')) }}">График монтажей</a></li>
                             href="{{ route('schedule.index', session('gp_schedule')) }}">График монтажей</a></li>
 
 
 
 
-    @if(hasrole('admin'))
+    @if(hasRole('admin'))
         <li class="nav-item"><a class="nav-link @if($active == 'maf_order') active @endif"
         <li class="nav-item"><a class="nav-link @if($active == 'maf_order') active @endif"
                                 href="{{ route('maf_order.index', session('gp_maf_order')) }}">Заказы МАФ</a></li>
                                 href="{{ route('maf_order.index', session('gp_maf_order')) }}">Заказы МАФ</a></li>
+    @endif
+    @if(auth()->user()?->role === \App\Models\Role::ADMIN)
         <li class="nav-item dropdown">
         <li class="nav-item dropdown">
             <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
             <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
                aria-haspopup="true" aria-expanded="false">
                aria-haspopup="true" aria-expanded="false">

+ 45 - 0
resources/views/maf_orders/index.blade.php

@@ -10,6 +10,11 @@
             @include('partials.year-switcher')
             @include('partials.year-switcher')
         </div>
         </div>
         <div class="col-md-4 text-end">
         <div class="col-md-4 text-end">
+            @if(hasRole('admin,assistant_head'))
+                <button type="button" class="btn btn-sm btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#setOrderInStockModal">
+                    Весь заказ на складе
+                </button>
+            @endif
             <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
             <button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
                 Добавить
                 Добавить
             </button>
             </button>
@@ -52,6 +57,38 @@
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>
+
+    @if(hasRole('admin,assistant_head'))
+        <div class="modal fade" id="setOrderInStockModal" tabindex="-1" aria-labelledby="setOrderInStockModalLabel" aria-hidden="true">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <form action="{{ route('maf_order.set_order_in_stock') }}" method="post">
+                        @csrf
+                        <div class="modal-header">
+                            <h1 class="modal-title fs-5" id="setOrderInStockModalLabel">Отметить заказ как "На складе"</h1>
+                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
+                        </div>
+                        <div class="modal-body">
+                            <label for="bulk_order_number" class="form-label">Номер заказа</label>
+                            <select class="form-select @error('bulk_order_number') is-invalid @enderror" id="bulk_order_number" name="bulk_order_number" required>
+                                <option value="">Выберите номер заказа</option>
+                                @foreach($order_numbers as $order_number)
+                                    <option value="{{ $order_number }}" @selected(old('bulk_order_number') === $order_number)>{{ $order_number }}</option>
+                                @endforeach
+                            </select>
+                            @error('bulk_order_number')
+                            <div class="invalid-feedback d-block">{{ $message }}</div>
+                            @enderror
+                        </div>
+                        <div class="modal-footer">
+                            <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Отмена</button>
+                            <button type="submit" class="btn btn-primary btn-sm">Сменить статус на складе</button>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    @endif
 @endsection
 @endsection
 @push('scripts')
 @push('scripts')
     <script type="module">
     <script type="module">
@@ -61,5 +98,13 @@
             $('#select_maf_form').slideUp();
             $('#select_maf_form').slideUp();
             $('#sku_form').slideDown();
             $('#sku_form').slideDown();
         });
         });
+
+        @if($errors->has('bulk_order_number') && hasRole('admin,assistant_head'))
+            const setOrderInStockModalElement = document.getElementById('setOrderInStockModal');
+            if (setOrderInStockModalElement) {
+                const setOrderInStockModal = new bootstrap.Modal(setOrderInStockModalElement);
+                setOrderInStockModal.show();
+            }
+        @endif
     </script>
     </script>
 @endpush
 @endpush

+ 14 - 12
resources/views/orders/show.blade.php

@@ -70,17 +70,19 @@
                 @include('partials.input', ['name' => 'tg_group_name', 'title' => 'Название группы в ТГ', 'value' => $order->tg_group_name ?? old('tg_group_name'), 'classes' => ['update-once'], 'disabled' => !hasRole('admin,manager')])
                 @include('partials.input', ['name' => 'tg_group_name', 'title' => 'Название группы в ТГ', 'value' => $order->tg_group_name ?? old('tg_group_name'), 'classes' => ['update-once'], 'disabled' => !hasRole('admin,manager')])
                 @include('partials.input', ['name' => 'tg_group_link', 'title' => 'https://t.me/', 'value' => $order->tg_group_link ?? old('tg_group_link'), 'classes' => ['update-once'], 'button' => (!empty($order->tg_group_link)) ? 'tg' : null, 'buttonText' => '<i class="bi bi-telegram"></i>', 'disabled' => !hasRole('admin,manager')])
                 @include('partials.input', ['name' => 'tg_group_link', 'title' => 'https://t.me/', 'value' => $order->tg_group_link ?? old('tg_group_link'), 'classes' => ['update-once'], 'button' => (!empty($order->tg_group_link)) ? 'tg' : null, 'buttonText' => '<i class="bi bi-telegram"></i>', 'disabled' => !hasRole('admin,manager')])
 
 
-                <hr>
-                <div class="reclamations">
-                    Рекламации
-                    @foreach($order->reclamations as $reclamation)
-                        <div>
-                            <a href="{{ route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => url()->current()]) }}">
-                                Рекламация № {{ $reclamation->id }} от {{ $reclamation->create_date }}
-                            </a>
-                        </div>
-                    @endforeach
-                </div>
+                @if(!hasRole('brigadier'))
+                    <hr>
+                    <div class="reclamations">
+                        Рекламации
+                        @foreach($order->reclamations as $reclamation)
+                            <div>
+                                <a href="{{ route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => url()->current()]) }}">
+                                    Рекламация № {{ $reclamation->id }} от {{ $reclamation->create_date }}
+                                </a>
+                            </div>
+                        @endforeach
+                    </div>
+                @endif
                 @if(hasRole('admin,manager'))
                 @if(hasRole('admin,manager'))
                     <hr>
                     <hr>
                     <div class="documents">
                     <div class="documents">
@@ -204,7 +206,7 @@
                                    data-toggle="lightbox" data-gallery="photos" data-size="fullscreen">
                                    data-toggle="lightbox" data-gallery="photos" data-size="fullscreen">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                 </a>
                                 </a>
-                                @if(hasRole('admin'))
+                                @if(hasRole('admin,manager'))
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        title="Удалить"></i>
                                        title="Удалить"></i>

+ 9 - 8
resources/views/products_sku/index.blade.php

@@ -60,15 +60,16 @@
                 <div class="modal-body">
                 <div class="modal-body">
                     <form action="{{ route('mafs.export') }}" method="post" enctype="multipart/form-data">
                     <form action="{{ route('mafs.export') }}" method="post" enctype="multipart/form-data">
                         @csrf
                         @csrf
+                        @include('partials.checkbox', ['title' => 'С учётом текущего фильтра и поиска', 'name' => 'withFilter', 'type' => 'checkbox', 'value' => 'yes', 'checked' => false])
                         <div class="d-none">
                         <div class="d-none">
-{{--                            @if(request()->s)--}}
-{{--                                @include('partials.input', ['name' => 'filters[s]', 'title' => 'поиск', 'value' => request()->s])--}}
-{{--                            @endif--}}
-{{--                            @if(request()->filters)--}}
-{{--                                @foreach(request()->filters as $filterName => $filterValue)--}}
-{{--                                    @include('partials.input', ['name' => 'filters[' . $filterName .']', 'title' => $filterName, 'value' => $filterValue])--}}
-{{--                                @endforeach--}}
-{{--                            @endif--}}
+                            @if(request()->s)
+                                @include('partials.input', ['name' => 's', 'title' => 'поиск', 'value' => request()->s])
+                            @endif
+                            @if(request()->filters)
+                                @foreach(request()->filters as $filterName => $filterValue)
+                                    @include('partials.input', ['name' => 'filters[' . $filterName .']', 'title' => $filterName, 'value' => $filterValue])
+                                @endforeach
+                            @endif
                         </div>
                         </div>
                         @include('partials.submit', ['name' => 'Экспорт'])
                         @include('partials.submit', ['name' => 'Экспорт'])
                     </form>
                     </form>

+ 29 - 21
resources/views/reclamations/edit.blade.php

@@ -479,7 +479,7 @@
                 <hr>
                 <hr>
                 <div class="acts">
                 <div class="acts">
                     Акты
                     Акты
-                    @if(hasRole('admin,manager'))
+                    @if(hasRole('admin,manager,brigadier,warehouse_head'))
                         <button class="btn btn-sm text-success" onclick="$('#upl-acts').trigger('click');"><i
                         <button class="btn btn-sm text-success" onclick="$('#upl-acts').trigger('click');"><i
                                     class="bi bi-plus-circle-fill"></i> Загрузить
                                     class="bi bi-plus-circle-fill"></i> Загрузить
                         </button>
                         </button>
@@ -515,21 +515,25 @@
                 <div class="photo_before">
                 <div class="photo_before">
                     <a href="#photos_before" data-bs-toggle="collapse">Фотографии проблемы
                     <a href="#photos_before" data-bs-toggle="collapse">Фотографии проблемы
                         ({{ $reclamation->photos_before->count() }})</a>
                         ({{ $reclamation->photos_before->count() }})</a>
-                    <button class="btn btn-sm text-success" onclick="$('#upl-photo-before').trigger('click');"><i
-                                class="bi bi-plus-circle-fill"></i> Загрузить
-                    </button>
+                    @if(hasRole('admin,manager'))
+                        <button class="btn btn-sm text-success" onclick="$('#upl-photo-before').trigger('click');"><i
+                                    class="bi bi-plus-circle-fill"></i> Загрузить
+                        </button>
+                    @endif
                     @if($reclamation->photos_before->count())
                     @if($reclamation->photos_before->count())
                         <a href="{{ route('reclamation.generate-photos-before-pack', $reclamation) }}"
                         <a href="{{ route('reclamation.generate-photos-before-pack', $reclamation) }}"
                            class="btn btn-sm text-primary"><i
                            class="btn btn-sm text-primary"><i
                                     class="bi bi-download"></i> Скачать все
                                     class="bi bi-download"></i> Скачать все
                         </a>
                         </a>
                     @endif
                     @endif
-                    <form action="{{ route('reclamations.upload-photo-before', $reclamation) }}"
-                          enctype="multipart/form-data" method="post" class="visually-hidden">
-                        @csrf
-                        <input required type="file" id="upl-photo-before" onchange="$(this).parent().submit()" multiple
-                               name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
-                    </form>
+                    @if(hasRole('admin,manager'))
+                        <form action="{{ route('reclamations.upload-photo-before', $reclamation) }}"
+                              enctype="multipart/form-data" method="post" class="visually-hidden">
+                            @csrf
+                            <input required type="file" id="upl-photo-before" onchange="$(this).parent().submit()" multiple
+                                   name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
+                        </form>
+                    @endif
                     <div class="row my-2 g-1 collapse" id="photos_before">
                     <div class="row my-2 g-1 collapse" id="photos_before">
                         @foreach($reclamation->photos_before as $photo)
                         @foreach($reclamation->photos_before as $photo)
                             <div class="col-4">
                             <div class="col-4">
@@ -537,7 +541,7 @@
                                    data-toggle="lightbox" data-gallery="photos-before" data-size="fullscreen">
                                    data-toggle="lightbox" data-gallery="photos-before" data-size="fullscreen">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                 </a>
                                 </a>
-                                @if(hasRole('admin'))
+                                @if(hasRole('admin,manager'))
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        title="Удалить"></i>
                                        title="Удалить"></i>
@@ -555,9 +559,11 @@
                 <div class="photo_after">
                 <div class="photo_after">
                     <a href="#photos_after" data-bs-toggle="collapse">Фотографии после устранения
                     <a href="#photos_after" data-bs-toggle="collapse">Фотографии после устранения
                         ({{ $reclamation->photos_after->count() }})</a>
                         ({{ $reclamation->photos_after->count() }})</a>
-                    <button class="btn btn-sm text-success" onclick="$('#upl-photo-after').trigger('click');"><i
-                                class="bi bi-plus-circle-fill"></i> Загрузить
-                    </button>
+                    @if(hasRole('admin,manager,brigadier,warehouse_head'))
+                        <button class="btn btn-sm text-success" onclick="$('#upl-photo-after').trigger('click');"><i
+                                    class="bi bi-plus-circle-fill"></i> Загрузить
+                        </button>
+                    @endif
                     @if($reclamation->photos_after->count())
                     @if($reclamation->photos_after->count())
                         <a href="{{ route('reclamation.generate-photos-after-pack', $reclamation) }}"
                         <a href="{{ route('reclamation.generate-photos-after-pack', $reclamation) }}"
                            class="btn btn-sm text-primary"><i
                            class="btn btn-sm text-primary"><i
@@ -565,12 +571,14 @@
                         </a>
                         </a>
                     @endif
                     @endif
 
 
-                    <form action="{{ route('reclamations.upload-photo-after', $reclamation) }}"
-                          enctype="multipart/form-data" method="post" class="visually-hidden">
-                        @csrf
-                        <input required type="file" id="upl-photo-after" onchange="$(this).parent().submit()" multiple
-                               name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
-                    </form>
+                    @if(hasRole('admin,manager,brigadier,warehouse_head'))
+                        <form action="{{ route('reclamations.upload-photo-after', $reclamation) }}"
+                              enctype="multipart/form-data" method="post" class="visually-hidden">
+                            @csrf
+                            <input required type="file" id="upl-photo-after" onchange="$(this).parent().submit()" multiple
+                                   name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
+                        </form>
+                    @endif
                     <div class="row my-2 g-1 collapse" id="photos_after">
                     <div class="row my-2 g-1 collapse" id="photos_after">
                         @foreach($reclamation->photos_after as $photo)
                         @foreach($reclamation->photos_after as $photo)
                             <div class="col-4">
                             <div class="col-4">
@@ -578,7 +586,7 @@
                                    data-toggle="lightbox" data-gallery="photos-after" data-size="fullscreen">
                                    data-toggle="lightbox" data-gallery="photos-after" data-size="fullscreen">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                 </a>
                                 </a>
-                                @if(hasRole('admin'))
+                                @if(hasRole('admin,manager'))
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        title="Удалить"></i>
                                        title="Удалить"></i>

+ 10 - 5
routes/web.php

@@ -162,6 +162,7 @@ Route::middleware('auth:web')->group(function () {
 
 
         Route::post('order/{order}/upload-document', [OrderController::class, 'uploadDocument'])->name('order.upload-document');
         Route::post('order/{order}/upload-document', [OrderController::class, 'uploadDocument'])->name('order.upload-document');
         Route::post('order/{order}/upload-statement', [OrderController::class, 'uploadStatement'])->name('order.upload-statement');
         Route::post('order/{order}/upload-statement', [OrderController::class, 'uploadStatement'])->name('order.upload-statement');
+        Route::delete('order/delete-photo/{order}/{file}', [OrderController::class, 'deletePhoto'])->name('order.delete-photo');
 
 
         Route::post('catalog/{product}/upload-certificate', [ProductController::class, 'uploadCertificate'])->name('catalog.upload-certificate');
         Route::post('catalog/{product}/upload-certificate', [ProductController::class, 'uploadCertificate'])->name('catalog.upload-certificate');
         Route::post('catalog/{product}/upload-thumbnail', [ProductController::class, 'uploadThumbnail'])->name('catalog.upload-thumbnail');
         Route::post('catalog/{product}/upload-thumbnail', [ProductController::class, 'uploadThumbnail'])->name('catalog.upload-thumbnail');
@@ -184,7 +185,8 @@ Route::middleware('auth:web')->group(function () {
         Route::post('reclamations/update/{reclamation}', [ReclamationController::class, 'update'])->name('reclamations.update');
         Route::post('reclamations/update/{reclamation}', [ReclamationController::class, 'update'])->name('reclamations.update');
         Route::post('reclamations/{reclamation}/update-status', [ReclamationController::class, 'updateStatus'])->name('reclamations.update-status');
         Route::post('reclamations/{reclamation}/update-status', [ReclamationController::class, 'updateStatus'])->name('reclamations.update-status');
         Route::post('reclamations/{reclamation}/upload-document', [ReclamationController::class, 'uploadDocument'])->name('reclamations.upload-document');
         Route::post('reclamations/{reclamation}/upload-document', [ReclamationController::class, 'uploadDocument'])->name('reclamations.upload-document');
-        Route::post('reclamations/{reclamation}/upload-act', [ReclamationController::class, 'uploadAct'])->name('reclamations.upload-act');
+        Route::delete('reclamations/delete-photo-before/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoBefore'])->name('reclamations.delete-photo-before');
+        Route::delete('reclamations/delete-photo-after/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoAfter'])->name('reclamations.delete-photo-after');
         Route::post('reclamations/{reclamation}/update-details', [ReclamationController::class, 'updateDetails'])->name('reclamations.update-details');
         Route::post('reclamations/{reclamation}/update-details', [ReclamationController::class, 'updateDetails'])->name('reclamations.update-details');
         Route::post('reclamations/{reclamation}/update-spare-parts', [ReclamationController::class, 'updateSpareParts'])->name('reclamations.update-spare-parts');
         Route::post('reclamations/{reclamation}/update-spare-parts', [ReclamationController::class, 'updateSpareParts'])->name('reclamations.update-spare-parts');
         Route::delete('reclamations/{reclamation}', [ReclamationController::class, 'delete'])->name('reclamations.delete')->middleware('role:' . Role::ADMIN);
         Route::delete('reclamations/{reclamation}', [ReclamationController::class, 'delete'])->name('reclamations.delete')->middleware('role:' . Role::ADMIN);
@@ -223,6 +225,7 @@ Route::middleware('auth:web')->group(function () {
         Route::post('maf_orders/update/{maf_order}', [MafOrderController::class, 'update'])->name('maf_order.update');
         Route::post('maf_orders/update/{maf_order}', [MafOrderController::class, 'update'])->name('maf_order.update');
         Route::delete('maf_orders/delete/{maf_order}', [MafOrderController::class, 'destroy'])->name('maf_order.delete');
         Route::delete('maf_orders/delete/{maf_order}', [MafOrderController::class, 'destroy'])->name('maf_order.delete');
         Route::post('maf_orders/set_in_stock/{maf_order}', [MafOrderController::class, 'setInStock'])->name('maf_order.set_in_stock');
         Route::post('maf_orders/set_in_stock/{maf_order}', [MafOrderController::class, 'setInStock'])->name('maf_order.set_in_stock');
+        Route::post('maf_orders/set_order_in_stock', [MafOrderController::class, 'setOrderInStock'])->name('maf_order.set_order_in_stock');
 
 
         // график
         // график
         Route::post('schedule/create_from_order', [ScheduleController::class, 'createFromOrder'])->name('schedule.create-from-order');
         Route::post('schedule/create_from_order', [ScheduleController::class, 'createFromOrder'])->name('schedule.create-from-order');
@@ -240,7 +243,6 @@ Route::middleware('auth:web')->group(function () {
         Route::post('order/move-maf', [OrderController::class, 'moveMaf'])->name('order.move-maf');
         Route::post('order/move-maf', [OrderController::class, 'moveMaf'])->name('order.move-maf');
         Route::post('order/create-ttn', [OrderController::class, 'createTtn'])->name('order.create-ttn');
         Route::post('order/create-ttn', [OrderController::class, 'createTtn'])->name('order.create-ttn');
 
 
-        Route::delete('order/delete-photo/{order}/{file}', [OrderController::class, 'deletePhoto'])->name('order.delete-photo');
         Route::delete('order/delete-document/{order}/{file}', [OrderController::class, 'deleteDocument'])->name('order.delete-document');
         Route::delete('order/delete-document/{order}/{file}', [OrderController::class, 'deleteDocument'])->name('order.delete-document');
         Route::delete('order/delete-statement/{order}/{file}', [OrderController::class, 'deleteStatement'])->name('order.delete-statement');
         Route::delete('order/delete-statement/{order}/{file}', [OrderController::class, 'deleteStatement'])->name('order.delete-statement');
         Route::delete('order/delete-all-photos/{order}', [OrderController::class, 'deleteAllPhotos'])->name('order.delete-all-photos');
         Route::delete('order/delete-all-photos/{order}', [OrderController::class, 'deleteAllPhotos'])->name('order.delete-all-photos');
@@ -249,10 +251,7 @@ Route::middleware('auth:web')->group(function () {
         Route::delete('catalog/delete-certificate/{product}/{file}', [ProductController::class, 'deleteCertificate'])->name('catalog.delete-certificate');
         Route::delete('catalog/delete-certificate/{product}/{file}', [ProductController::class, 'deleteCertificate'])->name('catalog.delete-certificate');
         Route::delete('product_sku/delete-passport/{product_sku}/{file}', [ProductSKUController::class, 'deletePassport'])->name('product-sku.delete-passport');
         Route::delete('product_sku/delete-passport/{product_sku}/{file}', [ProductSKUController::class, 'deletePassport'])->name('product-sku.delete-passport');
 
 
-        Route::delete('reclamations/delete-photo-before/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoBefore'])->name('reclamations.delete-photo-before');
-        Route::delete('reclamations/delete-photo-after/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoAfter'])->name('reclamations.delete-photo-after');
         Route::delete('reclamations/delete-document/{reclamation}/{file}', [ReclamationController::class, 'deleteDocument'])->name('reclamations.delete-document');
         Route::delete('reclamations/delete-document/{reclamation}/{file}', [ReclamationController::class, 'deleteDocument'])->name('reclamations.delete-document');
-        Route::delete('reclamations/delete-act/{reclamation}/{file}', [ReclamationController::class, 'deleteAct'])->name('reclamations.delete-act');
     });
     });
 
 
 
 
@@ -266,6 +265,12 @@ Route::middleware('auth:web')->group(function () {
 
 
     Route::post('reclamations/{reclamation}/upload-photo-before', [ReclamationController::class, 'uploadPhotoBefore'])->name('reclamations.upload-photo-before');
     Route::post('reclamations/{reclamation}/upload-photo-before', [ReclamationController::class, 'uploadPhotoBefore'])->name('reclamations.upload-photo-before');
     Route::post('reclamations/{reclamation}/upload-photo-after', [ReclamationController::class, 'uploadPhotoAfter'])->name('reclamations.upload-photo-after');
     Route::post('reclamations/{reclamation}/upload-photo-after', [ReclamationController::class, 'uploadPhotoAfter'])->name('reclamations.upload-photo-after');
+    Route::post('reclamations/{reclamation}/upload-act', [ReclamationController::class, 'uploadAct'])
+        ->name('reclamations.upload-act')
+        ->middleware('role:admin,manager,brigadier,' . Role::WAREHOUSE_HEAD);
+    Route::delete('reclamations/delete-act/{reclamation}/{file}', [ReclamationController::class, 'deleteAct'])
+        ->name('reclamations.delete-act')
+        ->middleware('role:' . Role::ADMIN);
 
 
     Route::get('schedule', [ScheduleController::class, 'index'])->name('schedule.index');
     Route::get('schedule', [ScheduleController::class, 'index'])->name('schedule.index');
 
 

BIN
templates/Passport.xlsx


+ 64 - 0
tests/Feature/MafOrderControllerTest.php

@@ -17,6 +17,7 @@ class MafOrderControllerTest extends TestCase
 
 
     private User $adminUser;
     private User $adminUser;
     private User $managerUser;
     private User $managerUser;
+    private User $assistantHeadUser;
 
 
     protected function setUp(): void
     protected function setUp(): void
     {
     {
@@ -24,6 +25,7 @@ class MafOrderControllerTest extends TestCase
 
 
         $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
         $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
         $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
         $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
+        $this->assistantHeadUser = User::factory()->create(['role' => Role::ASSISTANT_HEAD]);
     }
     }
 
 
     // ==================== Authentication ====================
     // ==================== Authentication ====================
@@ -52,6 +54,15 @@ class MafOrderControllerTest extends TestCase
         $response->assertStatus(403);
         $response->assertStatus(403);
     }
     }
 
 
+    public function test_assistant_head_can_access_maf_orders_index(): void
+    {
+        $response = $this->actingAs($this->assistantHeadUser)
+            ->get(route('maf_order.index'));
+
+        $response->assertStatus(200);
+        $response->assertViewIs('maf_orders.index');
+    }
+
     // ==================== Index ====================
     // ==================== Index ====================
 
 
     public function test_maf_orders_index_displays_orders(): void
     public function test_maf_orders_index_displays_orders(): void
@@ -231,4 +242,57 @@ class MafOrderControllerTest extends TestCase
 
 
         $response->assertRedirect(route('login'));
         $response->assertRedirect(route('login'));
     }
     }
+
+    public function test_set_order_in_stock_updates_all_rows_by_order_number(): void
+    {
+        $first = MafOrder::factory()->create([
+            'order_number' => 'MO-BULK-001',
+            'quantity' => 3,
+            'in_stock' => 0,
+            'status' => 'заказан',
+        ]);
+        $second = MafOrder::factory()->create([
+            'order_number' => 'MO-BULK-001',
+            'quantity' => 7,
+            'in_stock' => 1,
+            'status' => 'заказан',
+        ]);
+        $other = MafOrder::factory()->create([
+            'order_number' => 'MO-BULK-002',
+            'quantity' => 9,
+            'in_stock' => 2,
+            'status' => 'заказан',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('maf_order.set_order_in_stock'), [
+                'bulk_order_number' => 'MO-BULK-001',
+            ]);
+
+        $response->assertRedirect(route('maf_order.index'));
+        $this->assertDatabaseHas('maf_orders', [
+            'id' => $first->id,
+            'in_stock' => 3,
+            'status' => 'на складе',
+        ]);
+        $this->assertDatabaseHas('maf_orders', [
+            'id' => $second->id,
+            'in_stock' => 7,
+            'status' => 'на складе',
+        ]);
+        $this->assertDatabaseHas('maf_orders', [
+            'id' => $other->id,
+            'in_stock' => 2,
+            'status' => 'заказан',
+        ]);
+    }
+
+    public function test_guest_cannot_set_order_in_stock(): void
+    {
+        $response = $this->post(route('maf_order.set_order_in_stock'), [
+            'bulk_order_number' => 'MO-BULK-001',
+        ]);
+
+        $response->assertRedirect(route('login'));
+    }
 }
 }

+ 15 - 1
tests/Unit/Helpers/RolesHelperTest.php

@@ -20,6 +20,8 @@ class RolesHelperTest extends TestCase
         $this->assertArrayHasKey(Role::ADMIN, $roles);
         $this->assertArrayHasKey(Role::ADMIN, $roles);
         $this->assertArrayHasKey(Role::MANAGER, $roles);
         $this->assertArrayHasKey(Role::MANAGER, $roles);
         $this->assertArrayHasKey(Role::BRIGADIER, $roles);
         $this->assertArrayHasKey(Role::BRIGADIER, $roles);
+        $this->assertArrayHasKey(Role::WAREHOUSE_HEAD, $roles);
+        $this->assertArrayHasKey(Role::ASSISTANT_HEAD, $roles);
     }
     }
 
 
     public function test_get_roles_returns_name_by_key(): void
     public function test_get_roles_returns_name_by_key(): void
@@ -27,6 +29,8 @@ class RolesHelperTest extends TestCase
         $this->assertEquals('Админ', getRoles(Role::ADMIN));
         $this->assertEquals('Админ', getRoles(Role::ADMIN));
         $this->assertEquals('Менеджер', getRoles(Role::MANAGER));
         $this->assertEquals('Менеджер', getRoles(Role::MANAGER));
         $this->assertEquals('Бригадир', getRoles(Role::BRIGADIER));
         $this->assertEquals('Бригадир', getRoles(Role::BRIGADIER));
+        $this->assertEquals('Рук. Склада', getRoles(Role::WAREHOUSE_HEAD));
+        $this->assertEquals('Помощник рук.', getRoles(Role::ASSISTANT_HEAD));
     }
     }
 
 
     public function test_get_roles_returns_all_when_key_not_found(): void
     public function test_get_roles_returns_all_when_key_not_found(): void
@@ -34,7 +38,7 @@ class RolesHelperTest extends TestCase
         $result = getRoles('nonexistent');
         $result = getRoles('nonexistent');
 
 
         $this->assertIsArray($result);
         $this->assertIsArray($result);
-        $this->assertCount(3, $result);
+        $this->assertCount(5, $result);
     }
     }
 
 
     public function test_has_role_returns_true_for_correct_role(): void
     public function test_has_role_returns_true_for_correct_role(): void
@@ -61,6 +65,14 @@ class RolesHelperTest extends TestCase
         $this->assertTrue(hasRole(Role::MANAGER . ',' . Role::BRIGADIER, $user));
         $this->assertTrue(hasRole(Role::MANAGER . ',' . Role::BRIGADIER, $user));
     }
     }
 
 
+    public function test_assistant_head_has_admin_permissions_in_helper(): void
+    {
+        $user = new \App\Models\User();
+        $user->role = Role::ASSISTANT_HEAD;
+
+        $this->assertTrue(hasRole(Role::ADMIN, $user));
+    }
+
     public function test_has_role_returns_false_when_user_null(): void
     public function test_has_role_returns_false_when_user_null(): void
     {
     {
         $this->assertFalse(hasRole(Role::ADMIN, null));
         $this->assertFalse(hasRole(Role::ADMIN, null));
@@ -71,6 +83,8 @@ class RolesHelperTest extends TestCase
         $this->assertEquals('Админ', roleName(Role::ADMIN));
         $this->assertEquals('Админ', roleName(Role::ADMIN));
         $this->assertEquals('Менеджер', roleName(Role::MANAGER));
         $this->assertEquals('Менеджер', roleName(Role::MANAGER));
         $this->assertEquals('Бригадир', roleName(Role::BRIGADIER));
         $this->assertEquals('Бригадир', roleName(Role::BRIGADIER));
+        $this->assertEquals('Рук. Склада', roleName(Role::WAREHOUSE_HEAD));
+        $this->assertEquals('Помощник рук.', roleName(Role::ASSISTANT_HEAD));
     }
     }
 
 
     public function test_file_name_replaces_special_chars(): void
     public function test_file_name_replaces_special_chars(): void