Переглянути джерело

Block in-mount status without linked MAF orders

Alexander Musikhin 4 днів тому
батько
коміт
f8e7317961

+ 27 - 0
app/Http/Controllers/OrderController.php

@@ -157,6 +157,12 @@ class OrderController extends Controller
         if(isset($data['id'])) {
             $order = Order::query()->where('id', $data['id'])->first();
             $status = $order->order_status_id;
+            $statusErrors = $this->validateMountStatusChange($order, $data['order_status_id'] ?? null);
+
+            if ($statusErrors !== []) {
+                return $this->mountStatusValidationFailed($request, $statusErrors);
+            }
+
             $order->update($data);
             $order->refresh();
             if($order->order_status_id != $status) {
@@ -260,6 +266,27 @@ class OrderController extends Controller
         return redirect()->route('order.show', ['order' => $order, 'previous_url' => $request->get('previous_url')]);
     }
 
+    private function validateMountStatusChange(Order $order, mixed $newStatusId): array
+    {
+        if ((int) $newStatusId !== Order::STATUS_IN_MOUNT) {
+            return [];
+        }
+
+        return $order->getMountStatusErrors();
+    }
+
+    private function mountStatusValidationFailed(Request $request, array $errors)
+    {
+        if ($request->expectsJson() || $request->ajax()) {
+            return response()->json([
+                'message' => $errors[0],
+                'errors' => $errors,
+            ], 422);
+        }
+
+        return redirect()->back()->withInput()->with(['danger' => $errors]);
+    }
+
     /**
      * Display the specified resource.
      */

+ 11 - 10
app/Http/Controllers/ScheduleController.php

@@ -158,15 +158,6 @@ class ScheduleController extends Controller
     {
         $validated = $request->validated();
 
-        // delete all auto schedules for this order
-        if(isset($validated['delete_old_records'])) {
-            Schedule::query()
-                ->where('order_id', $validated['order_id'])
-                ->where('source', 'Площадки')
-                ->where('manual', false)
-                ->delete();
-        }
-
         // create all records in schedule
         $order = Order::query()
             ->where('id', $validated['order_id'])
@@ -176,11 +167,21 @@ class ScheduleController extends Controller
         if(!$order->brigadier_id) $errors[] = 'Не указан бригадир!';
         if(!$order->installation_date) $errors[] = 'Не указана дата монтажа!';
         if(!$order->install_days) $errors[] = 'Не указан срок монтажа!';
+        $errors = array_merge($errors, $order->getMountStatusErrors());
 
         if($errors) {
             return redirect()->route('order.show', $order->id)->with(['danger' => $errors]);
         }
 
+        // delete all auto schedules for this order
+        if(isset($validated['delete_old_records'])) {
+            Schedule::query()
+                ->where('order_id', $validated['order_id'])
+                ->where('source', 'Площадки')
+                ->where('manual', false)
+                ->delete();
+        }
+
         if(empty($validated['skus'])) {
             foreach ($order->products_sku as $psku)
                 $validated['skus'][] = $psku->id;
@@ -219,7 +220,7 @@ class ScheduleController extends Controller
                     'mafs' => $mafsText,
                     'mafs_count' => $mafsCount,
                     'brigadier_id' => $order->brigadier_id,
-                    'comment'   => $validated['comment'],
+                    'comment'   => $validated['comment'] ?? null,
                 ]);
             if($first && isset($validated['send_notifications'])) {
                 $first = false;

+ 23 - 8
app/Models/Order.php

@@ -231,13 +231,7 @@ class Order extends Model
             && ($this->brigadier_id !== null)
             && ($this->installation_date !== null)
         ) {
-            $allMafConnected = true;
-            foreach ($this->products_sku as $sku) {
-                if($sku->maf_order) continue;
-                $allMafConnected = false;
-            }
-
-            if($allMafConnected) {
+            if ($this->canBeMovedToMountStatus()) {
                 $this->update(['order_status_id' => self::STATUS_IN_MOUNT]);
             }
         }
@@ -295,12 +289,33 @@ class Order extends Model
     {
         $errors = [];
         foreach($this->products_sku as $sku) {
-            if($sku->maf_order_id) continue;
+            $orderNumber = trim((string) ($sku->maf_order?->order_number ?? ''));
+            if($sku->maf_order_id && $orderNumber !== '') continue;
             $errors[] = 'МАФ ' . $sku->product->article . 'не имеет номера заказа МАФ!';
         }
         return $errors;
     }
 
+    public function getMountStatusErrors(): array
+    {
+        foreach ($this->products_sku as $sku) {
+            $orderNumber = trim((string) ($sku->maf_order?->order_number ?? ''));
+
+            if ($sku->maf_order_id !== null && $orderNumber !== '') {
+                continue;
+            }
+
+            return ['МАФ не привязан к заказу'];
+        }
+
+        return [];
+    }
+
+    public function canBeMovedToMountStatus(): bool
+    {
+        return $this->getMountStatusErrors() === [];
+    }
+
     public function canCreateHandover(): array
     {
         $errors = [];

+ 24 - 3
resources/views/orders/show.blade.php

@@ -559,9 +559,15 @@
             });
         @endif
 
+        $('.update-once').on('focus', function () {
+            $(this).data('previous-value', $(this).val());
+        });
+
         $('.update-once').on('change', function () {
-            let v = $(this).val();
-            let n = $(this).attr('name');
+            let $field = $(this);
+            let v = $field.val();
+            let n = $field.attr('name');
+            let previousValue = $field.data('previous-value');
             $.post(
                 '{{ route('order.update', $order->id) }}',
                 {
@@ -570,6 +576,7 @@
                     [n]: v
                 },
                 function () {
+                    $field.data('previous-value', v);
                     $('.alerts').append(
                         '<div class="main-alert alert alert-success" role="alert">Площадка обновлена!</div>'
                     );
@@ -579,7 +586,21 @@
                         })
                     }, 3000);
                 }
-            );
+            ).fail(function (xhr) {
+                if (previousValue !== undefined) {
+                    $field.val(previousValue);
+                }
+
+                let errorText = xhr.responseJSON?.message || 'Не удалось обновить площадку!';
+                $('.alerts').append(
+                    '<div class="main-alert alert alert-danger" role="alert">' + errorText + '</div>'
+                );
+                setTimeout(function () {
+                    $('.main-alert').fadeTo(2000, 500).slideUp(500, function () {
+                        $(".main-alert").slideUp(500);
+                    })
+                }, 3000);
+            });
         });
     </script>
 @endpush

+ 24 - 3
resources/views/partials/table.blade.php

@@ -585,9 +585,15 @@
             document.location.href = currentUrl.href;
         });
 
+        $('.change-order-status').on('focus', function () {
+            $(this).data('previous-value', $(this).val());
+        });
+
         $('.change-order-status').on('change', function () {
-            let orderStatusId = $(this).val();
-            let orderId = $(this).attr('data-order-id');
+            let $select = $(this);
+            let orderStatusId = $select.val();
+            let orderId = $select.attr('data-order-id');
+            let previousValue = $select.data('previous-value');
 
             $.post(
                 '{{ route('order.update') }}',
@@ -597,6 +603,7 @@
                     order_status_id: orderStatusId
                 },
                 function () {
+                    $select.data('previous-value', orderStatusId);
                     $('.alerts').append(
                         '<div class="main-alert alert alert-success" role="alert">Обновлён статус площадки!</div>'
                     );
@@ -606,7 +613,21 @@
                         })
                     }, 3000);
                 }
-            );
+            ).fail(function (xhr) {
+                if (previousValue !== undefined) {
+                    $select.val(previousValue);
+                }
+
+                let errorText = xhr.responseJSON?.message || 'Не удалось обновить статус площадки!';
+                $('.alerts').append(
+                    '<div class="main-alert alert alert-danger" role="alert">' + errorText + '</div>'
+                );
+                setTimeout(function () {
+                    $('.main-alert').fadeTo(2000, 500).slideUp(500, function () {
+                        $(".main-alert").slideUp(500);
+                    })
+                }, 3000);
+            });
         });
 
         $('.change-reclamation-status').on('change', function () {

+ 95 - 0
tests/Feature/OrderControllerTest.php

@@ -254,6 +254,101 @@ class OrderControllerTest extends TestCase
         ]);
     }
 
+    public function test_cannot_set_in_mount_status_when_order_has_unlinked_maf(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
+            'installation_date' => '2026-04-25',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => null,
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('order.update'), [
+                'id' => $order->id,
+                'order_status_id' => Order::STATUS_IN_MOUNT,
+            ]);
+
+        $response->assertRedirect();
+        $response->assertSessionHas('danger', function ($messages) {
+            return in_array('МАФ не привязан к заказу', (array) $messages, true);
+        });
+
+        $this->assertDatabaseHas('orders', [
+            'id' => $order->id,
+            'order_status_id' => Order::STATUS_READY_TO_MOUNT,
+        ]);
+    }
+
+    public function test_ajax_cannot_set_in_mount_status_when_maf_order_number_is_empty(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
+            'installation_date' => '2026-04-25',
+        ]);
+        $mafOrder = MafOrder::factory()->create([
+            'product_id' => $product->id,
+            'order_number' => '',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => $mafOrder->id,
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->withHeader('X-Requested-With', 'XMLHttpRequest')
+            ->post(route('order.update'), [
+                'id' => $order->id,
+                'order_status_id' => Order::STATUS_IN_MOUNT,
+            ]);
+
+        $response->assertStatus(422);
+        $response->assertJson([
+            'message' => 'МАФ не привязан к заказу',
+        ]);
+
+        $this->assertDatabaseHas('orders', [
+            'id' => $order->id,
+            'order_status_id' => Order::STATUS_READY_TO_MOUNT,
+        ]);
+    }
+
+    public function test_can_set_in_mount_status_when_all_mafs_are_linked_to_order_numbers(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
+            'installation_date' => '2026-04-25',
+        ]);
+        $mafOrder = MafOrder::factory()->create([
+            'product_id' => $product->id,
+            'order_number' => 'MO-7788',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => $mafOrder->id,
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('order.update'), [
+                'id' => $order->id,
+                'order_status_id' => Order::STATUS_IN_MOUNT,
+            ]);
+
+        $response->assertRedirect();
+        $this->assertDatabaseHas('orders', [
+            'id' => $order->id,
+            'order_status_id' => Order::STATUS_IN_MOUNT,
+        ]);
+    }
+
     public function test_manager_can_update_ready_date(): void
     {
         $order = Order::factory()->create([

+ 103 - 0
tests/Feature/ScheduleControllerTest.php

@@ -4,7 +4,10 @@ namespace Tests\Feature;
 
 use App\Models\Dictionary\Area;
 use App\Models\Dictionary\District;
+use App\Models\MafOrder;
 use App\Models\Order;
+use App\Models\Product;
+use App\Models\ProductSKU;
 use App\Models\Reclamation;
 use App\Models\Role;
 use App\Models\Schedule;
@@ -281,6 +284,106 @@ class ScheduleControllerTest extends TestCase
         ]);
     }
 
+    public function test_cannot_create_schedule_from_order_when_order_has_unlinked_maf(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
+            'installation_date' => '2026-04-22',
+            'install_days' => 2,
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => null,
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('schedule.create-from-order'), [
+                'order_id' => $order->id,
+                'delete_old_records' => '1',
+            ]);
+
+        $response->assertRedirect(route('order.show', $order->id));
+        $response->assertSessionHas('danger', function ($messages) {
+            return in_array('МАФ не привязан к заказу', (array) $messages, true);
+        });
+        $this->assertDatabaseCount('schedules', 0);
+    }
+
+    public function test_cannot_create_schedule_from_order_when_maf_order_number_is_empty(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
+            'installation_date' => '2026-04-22',
+            'install_days' => 2,
+        ]);
+        $mafOrder = MafOrder::factory()->create([
+            'product_id' => $product->id,
+            'order_number' => '',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => $mafOrder->id,
+        ]);
+
+        Schedule::factory()->create([
+            'order_id' => $order->id,
+            'source' => 'Площадки',
+            'manual' => false,
+            'brigadier_id' => $this->brigadierUser->id,
+            'object_address' => $order->object_address,
+            'installation_date' => '2026-04-20',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('schedule.create-from-order'), [
+                'order_id' => $order->id,
+                'delete_old_records' => '1',
+            ]);
+
+        $response->assertRedirect(route('order.show', $order->id));
+        $response->assertSessionHas('danger', function ($messages) {
+            return in_array('МАФ не привязан к заказу', (array) $messages, true);
+        });
+        $this->assertDatabaseCount('schedules', 1);
+    }
+
+    public function test_can_create_schedule_from_order_when_all_mafs_are_linked_to_order_numbers(): void
+    {
+        $product = Product::factory()->create();
+        $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
+            'installation_date' => '2026-04-22',
+            'install_days' => 2,
+        ]);
+        $mafOrder = MafOrder::factory()->create([
+            'product_id' => $product->id,
+            'order_number' => 'MO-2026-1',
+        ]);
+
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => $mafOrder->id,
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->post(route('schedule.create-from-order'), [
+                'order_id' => $order->id,
+                'comment' => '',
+            ]);
+
+        $response->assertRedirect(route('schedule.index'));
+        $this->assertDatabaseCount('schedules', 2);
+        $this->assertDatabaseHas('schedules', [
+            'order_id' => $order->id,
+            'source' => 'Площадки',
+            'manual' => false,
+        ]);
+    }
+
     // ==================== Delete ====================
 
     public function test_admin_can_delete_schedule(): void