Prechádzať zdrojové kódy

feat(maf): add bulk stock status update and role-based export

- Add \"Весь заказ на складе\" bulk action for admin/assistant_head roles
- Modify ExportMafService to accept integer year parameter directly
- Add assistant_head role tests for MafOrder controller
- Implement setAllOrdersInStock method for batch status updates
- Add role-based UI controls for bulk operations
Alexander Musikhin 1 týždeň pred
rodič
commit
a58a3fd441

+ 32 - 0
app/Http/Controllers/MafOrderController.php

@@ -6,6 +6,8 @@ use App\Http\Requests\StoreMafOrderRequest;
 use App\Models\MafOrder;
 use App\Models\MafOrdersView;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Validation\ValidationException;
 
 class MafOrderController extends Controller
 {
@@ -50,6 +52,13 @@ class MafOrderController extends Controller
         $this->applyStableSorting($q);
 
         $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);
     }
 
@@ -88,4 +97,27 @@ class MafOrderController extends Controller
         $maf_order->update(['in_stock' => $maf_order->quantity, 'status' => 'на складе']);
         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'));
+    }
 }

+ 4 - 1
app/Services/ExportMafService.php

@@ -18,8 +18,11 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 
 class ExportMafService
 {
-    public function handle(int $userId, array $filters = []): 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
         $inputFileName = './templates/Mafs.xlsx';

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

@@ -10,6 +10,11 @@
             @include('partials.year-switcher')
         </div>
         <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>
@@ -52,6 +57,38 @@
             </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
 @push('scripts')
     <script type="module">
@@ -61,5 +98,13 @@
             $('#select_maf_form').slideUp();
             $('#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>
 @endpush

+ 1 - 0
routes/web.php

@@ -225,6 +225,7 @@ Route::middleware('auth:web')->group(function () {
         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::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');

BIN
templates/Passport.xlsx


+ 64 - 0
tests/Feature/MafOrderControllerTest.php

@@ -17,6 +17,7 @@ class MafOrderControllerTest extends TestCase
 
     private User $adminUser;
     private User $managerUser;
+    private User $assistantHeadUser;
 
     protected function setUp(): void
     {
@@ -24,6 +25,7 @@ class MafOrderControllerTest extends TestCase
 
         $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
         $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
+        $this->assistantHeadUser = User::factory()->create(['role' => Role::ASSISTANT_HEAD]);
     }
 
     // ==================== Authentication ====================
@@ -52,6 +54,15 @@ class MafOrderControllerTest extends TestCase
         $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 ====================
 
     public function test_maf_orders_index_displays_orders(): void
@@ -231,4 +242,57 @@ class MafOrderControllerTest extends TestCase
 
         $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'));
+    }
 }