浏览代码

orders: manager access to ready date, object info, move-maf label

Alexander Musikhin 2 周之前
父节点
当前提交
560910c7aa

+ 8 - 6
app/Http/Controllers/OrderController.php

@@ -491,6 +491,7 @@ class OrderController extends Controller
     {
         $ret = [];
         $s = $request->get('s');
+        $currentOrderId = $request->integer('current_order_id');
         $searchFields = $this->data['searchFields'];
         $result = OrderView::query();
         if($s) {
@@ -501,9 +502,13 @@ class OrderController extends Controller
             });
         }
 
+        if ($currentOrderId > 0) {
+            $result->where('id', '!=', $currentOrderId);
+        }
+
         $result->orderBy('object_address');
         foreach ($result->get() as $p) {
-            $ret[$p->id] = $p->common_name;
+            $ret[$p->id] = $p->move_maf_name;
         }
         return $ret;
     }
@@ -511,7 +516,7 @@ class OrderController extends Controller
     public function uploadPhoto(Request $request, Order $order, FileService $fileService)
     {
         $data = $request->validate([
-            'photo.*' => 'mimes:jpeg,jpg,png',
+            'photo.*' => 'mimes:jpeg,jpg,png,webp',
         ]);
 
         try {
@@ -634,10 +639,7 @@ class OrderController extends Controller
 
     public function generateInstallationPack(Order $order)
     {
-        $errors = [];
-        if(!in_array($order->order_status_id, [Order::STATUS_READY_TO_MOUNT, Order::STATUS_IN_MOUNT]))
-            $errors[] = 'Статус должен быть "Готов к монтажу" или "В монтаже"!';
-        $errors = array_merge($errors, $order->isAllMafConnected());
+        $errors = $order->isAllMafConnected();
         if($errors) {
             return redirect()->route('order.show', $order)->with(['danger' => $errors]);
         }

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

@@ -229,7 +229,7 @@ class ReclamationController extends Controller
         $this->ensureCanViewReclamation($reclamation);
 
         $data = $request->validate([
-            'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
+            'photo.*' => 'mimes:jpeg,jpg,png,webp|max:8192',
         ]);
 
         try {
@@ -253,7 +253,7 @@ class ReclamationController extends Controller
         $this->ensureCanViewReclamation($reclamation);
 
         $data = $request->validate([
-            'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
+            'photo.*' => 'mimes:jpeg,jpg,png,webp|max:8192',
         ]);
 
         try {

+ 8 - 1
app/Models/Order.php

@@ -104,7 +104,7 @@ class Order extends Model
         'install_days',
     ];
 
-    public $appends = ['common_name', 'products_with_count'];
+    public $appends = ['common_name', 'move_maf_name', 'products_with_count'];
 
     public function products_sku(): HasMany
     {
@@ -251,6 +251,13 @@ class Order extends Model
         );
     }
 
+    public function moveMafName(): Attribute
+    {
+        return Attribute::make(
+            get: fn ($value) => trim($this->common_name . ($this->name ? ' | ' . $this->name : '')),
+        );
+    }
+
     public function productsWithCount(): Attribute
     {
         $products = $this->products_sku;

+ 8 - 1
app/Models/OrderView.php

@@ -40,7 +40,7 @@ class OrderView extends Model
         'order_status_name',
     ];
 
-    public $appends = ['common_name', 'products_with_count'];
+    public $appends = ['common_name', 'move_maf_name', 'products_with_count'];
 
 
     public function commonName(): Attribute
@@ -50,6 +50,13 @@ class OrderView extends Model
         );
     }
 
+    public function moveMafName(): Attribute
+    {
+        return Attribute::make(
+            get: fn ($value) => trim($this->common_name . ($this->name ? ' | ' . $this->name : '')),
+        );
+    }
+
     public function productsWithCount(): Attribute
     {
         $products = ProductSKU::query()->where('order_id', $this->id)->get(); //$this->products_sku;

+ 1 - 1
resources/views/orders/edit.blade.php

@@ -32,7 +32,7 @@
 
                 @include('partials.input', ['name' => 'install_days', 'title' => 'Дней на монтаж', 'type' => 'number', 'min' => 1, 'value' => $order->install_days ?? old('install_days'), 'disabled' => !hasRole('admin')])
 
-                @include('partials.input', ['name' => 'ready_date', 'title' => 'Дата готовности площадки', 'type' => 'date', 'value' => $order->ready_date ?? old('ready_date'), 'disabled' => !hasRole('admin')])
+                @include('partials.input', ['name' => 'ready_date', 'title' => 'Дата готовности площадки', 'type' => 'date', 'value' => $order->ready_date ?? old('ready_date'), 'disabled' => !hasRole('admin,manager')])
 
                 @include('partials.select', ['name' => 'brigadier_id', 'title' => 'Бригадир', 'options' => $brigadiers, 'value' => $order->brigadier_id ?? old('brigadier_id'), 'first_empty' => true, 'disabled' => !hasRole('admin')])
 

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

@@ -59,12 +59,14 @@
 
                 @include('partials.input',  ['name' => 'name', 'title' => 'Название', 'value' => $order->name ?? old('name'), 'required' => true, 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
                 @include('partials.input',  ['name' => 'object_address', 'title' => 'Адрес объекта', 'value' => $order->object_address ?? old('object_address'), 'required' => true, 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
+                @include('partials.input',  ['name' => 'district_name', 'title' => 'Округ', 'value' => $order->district?->name, 'disabled' => true])
+                @include('partials.input',  ['name' => 'area_name', 'title' => 'Район', 'value' => $order->area?->name, 'disabled' => true])
                 @include('partials.select', ['name' => 'object_type_id', 'title' => 'Тип объекта', 'options' => $objectTypes, 'value' => $order->object_type_id ?? old('object_type_id'), 'required' => true, 'first_empty' => true, 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
                 @include('partials.select', ['name' => 'order_status_id', 'title' => 'Статус', 'options' => $orderStatuses, 'value' => $order->order_status_id ?? old('order_status_id'), 'required' => true, 'classes' => ['update-once'], 'disabled' => !hasRole('admin,manager')])
                 @include('partials.textarea', ['name' => 'comment', 'title' => 'Комментарий', 'value' => $order->comment ?? old('comment'), 'classes' => ['update-once'], 'disabled' => !hasRole('admin,manager')])
                 @include('partials.input', ['name' => 'installation_date', 'title' => 'Дата выхода на монтаж', 'type' => 'date', 'value' => $order->installation_date ?? old('installation_date'), 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
                 @include('partials.input', ['name' => 'install_days', 'title' => 'Дней на монтаж', 'type' => 'number', 'min' => 1, 'value' => $order->install_days ?? old('install_days'), 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
-                @include('partials.input', ['name' => 'ready_date', 'title' => 'Дата готовности площадки', 'type' => 'date', 'value' => $order->ready_date ?? old('ready_date'), 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
+                @include('partials.input', ['name' => 'ready_date', 'title' => 'Дата готовности площадки', 'type' => 'date', 'value' => $order->ready_date ?? old('ready_date'), 'disabled' => !hasRole('admin,manager'), 'classes' => ['update-once']])
                 @include('partials.select', ['name' => 'brigadier_id', 'title' => 'Бригадир', 'options' => $brigadiers, 'value' => $order->brigadier_id ?? old('brigadier_id'), 'first_empty' => true, 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
                 @include('partials.select', ['name' => 'user_id', 'title' => 'Менеджер', 'options' => $users, 'value' => $order->user_id ?? old('user_id') ?? auth()->user()->id, 'disabled' => !hasRole('admin'), 'classes' => ['update-once']])
                 @include('partials.input', ['name' => 'tg_group_name', 'title' => 'Название группы в ТГ', 'value' => $order->tg_group_name ?? old('tg_group_name'), 'classes' => ['update-once'], 'disabled' => !hasRole('admin,manager')])
@@ -187,7 +189,7 @@
                           class="visually-hidden">
                         @csrf
                         <input required type="file" id="upl-photo" onchange="$(this).parent().submit()" multiple
-                               name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
+                               name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png,.webp">
                     </form>
                     @if(hasRole('admin'))
                         <button class="btn btn-sm text-danger" onclick="customConfirm('Удалить все фотографии?', function () { $('#delete-all-photos').submit(); }, 'Подтверждение удаления'); return false;"><i
@@ -449,7 +451,7 @@
             // select order
             $('#search_order').on('keyup', function () {
                 // search products on backend
-                $.get('{{ route('order.search') }}?s=' + $(this).val(),
+                $.get('{{ route('order.search') }}?s=' + $(this).val() + '&current_order_id={{ $order->id }}',
                     function (data) {
                         $('#select_order').children().remove()
                         $.each(data, function (id, name) {

+ 2 - 2
resources/views/reclamations/edit.blade.php

@@ -533,7 +533,7 @@
                               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">
+                                   name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png,.webp">
                         </form>
                     @endif
                     <div class="row my-2 g-1 collapse" id="photos_before">
@@ -578,7 +578,7 @@
                               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">
+                                   name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png,.webp">
                         </form>
                     @endif
                     <div class="row my-2 g-1 collapse" id="photos_after">

+ 125 - 12
tests/Feature/OrderControllerTest.php

@@ -2,6 +2,7 @@
 
 namespace Tests\Feature;
 
+use App\Jobs\GenerateInstallationPack;
 use App\Models\Dictionary\Area;
 use App\Models\Dictionary\District;
 use App\Models\File;
@@ -15,6 +16,7 @@ use App\Models\Role;
 use App\Models\User;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Facades\Bus;
 use Illuminate\Support\Facades\Storage;
 use Tests\TestCase;
 
@@ -194,6 +196,25 @@ class OrderControllerTest extends TestCase
         ]);
     }
 
+    public function test_manager_can_update_ready_date(): void
+    {
+        $order = Order::factory()->create([
+            'ready_date' => '2026-04-01',
+        ]);
+
+        $response = $this->actingAs($this->managerUser)
+            ->post(route('order.store'), [
+                'id' => $order->id,
+                'ready_date' => '2026-04-25',
+            ]);
+
+        $response->assertRedirect();
+        $this->assertDatabaseHas('orders', [
+            'id' => $order->id,
+            'ready_date' => '2026-04-25',
+        ]);
+    }
+
     // ==================== Show ====================
 
     public function test_can_view_order_details(): void
@@ -210,6 +231,26 @@ class OrderControllerTest extends TestCase
         $response->assertSee($order->object_address);
     }
 
+    public function test_order_details_show_area_and_district(): void
+    {
+        $district = District::factory()->create(['name' => 'Центральный округ']);
+        $area = Area::factory()->create([
+            'district_id' => $district->id,
+            'name' => 'Тверской район',
+        ]);
+        $order = Order::factory()->create([
+            'district_id' => $district->id,
+            'area_id' => $area->id,
+        ]);
+
+        $response = $this->actingAs($this->managerUser)
+            ->get(route('order.show', $order));
+
+        $response->assertOk();
+        $response->assertSee('Центральный округ');
+        $response->assertSee('Тверской район');
+    }
+
     public function test_brigadier_cannot_view_handed_over_order_details(): void
     {
         $order = Order::factory()->create([
@@ -236,6 +277,20 @@ class OrderControllerTest extends TestCase
         $response->assertViewIs('orders.edit');
     }
 
+    public function test_manager_can_edit_ready_date_in_order_edit_form(): void
+    {
+        $order = Order::factory()->create();
+
+        $response = $this->actingAs($this->managerUser)
+            ->get(route('order.edit', $order));
+
+        $response->assertOk();
+        $this->assertMatchesRegularExpression(
+            '/<input[^>]*name="ready_date"(?:(?!disabled).)*>/s',
+            $response->getContent()
+        );
+    }
+
     // ==================== Destroy ====================
 
     public function test_can_delete_order(): void
@@ -307,8 +362,39 @@ class OrderControllerTest extends TestCase
             ->getJson(route('order.search', ['s' => 'Менеджер Поиска']));
 
         $response->assertOk();
-        $response->assertJsonPath((string) $matchedOrder->id, $matchedOrder->common_name);
-        $response->assertJsonMissing([$otherOrder->id => $otherOrder->common_name]);
+        $response->assertJsonPath((string) $matchedOrder->id, $matchedOrder->move_maf_name);
+        $response->assertJsonMissing([$otherOrder->id => $otherOrder->move_maf_name]);
+    }
+
+    public function test_order_search_route_includes_site_name_and_excludes_current_order(): void
+    {
+        $district = District::factory()->create(['name' => 'Северный округ']);
+        $area = Area::factory()->create([
+            'district_id' => $district->id,
+            'name' => 'Левобережный',
+        ]);
+        $currentOrder = Order::factory()->create([
+            'district_id' => $district->id,
+            'area_id' => $area->id,
+            'name' => 'Текущая площадка',
+            'object_address' => 'ул. Проверочная, д. 1',
+        ]);
+        $targetOrder = Order::factory()->create([
+            'district_id' => $district->id,
+            'area_id' => $area->id,
+            'name' => 'Площадка назначения',
+            'object_address' => 'ул. Проверочная, д. 2',
+        ]);
+
+        $response = $this->actingAs($this->adminUser)
+            ->getJson(route('order.search', [
+                's' => 'Проверочная',
+                'current_order_id' => $currentOrder->id,
+            ]));
+
+        $response->assertOk();
+        $response->assertJsonPath((string) $targetOrder->id, $targetOrder->move_maf_name);
+        $response->assertJsonMissing([(string) $currentOrder->id => $currentOrder->move_maf_name]);
     }
 
     // ==================== MAF Operations ====================
@@ -673,42 +759,69 @@ class OrderControllerTest extends TestCase
         $this->assertCount(5, $order->fresh()->documents);
     }
 
+    public function test_can_upload_webp_photo(): void
+    {
+        Storage::fake('public');
+
+        $order = Order::factory()->create();
+        $photo = UploadedFile::fake()->create('photo.webp', 100, 'image/webp');
+
+        $response = $this->actingAs($this->managerUser)
+            ->post(route('order.upload-photo', $order), [
+                'photo' => [$photo],
+            ]);
+
+        $response->assertRedirect();
+        $saved = $order->fresh()->photos->first();
+        $this->assertNotNull($saved);
+        $this->assertSame('photo.webp', $saved->original_name);
+    }
+
     // ==================== Generation ====================
 
-    public function test_generate_installation_pack_requires_correct_status(): void
+    public function test_generate_installation_pack_is_allowed_for_any_status_when_data_is_valid(): void
     {
+        Bus::fake();
+
+        $product = Product::factory()->create();
+        $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
         $order = Order::factory()->create([
             'order_status_id' => Order::STATUS_NEW,
         ]);
+        ProductSKU::factory()->create([
+            'order_id' => $order->id,
+            'product_id' => $product->id,
+            'maf_order_id' => $mafOrder->id,
+        ]);
 
         $response = $this->actingAs($this->managerUser)
             ->get(route('order.generate-installation-pack', $order));
 
         $response->assertRedirect(route('order.show', $order));
-        $response->assertSessionHas('danger');
+        $response->assertSessionHas('success');
+        Bus::assertDispatched(GenerateInstallationPack::class);
     }
 
-    public function test_generate_installation_pack_succeeds_with_correct_status(): void
+    public function test_generate_installation_pack_still_requires_connected_maf(): void
     {
-        $product = Product::factory()->create();
-        $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
+        Bus::fake();
 
+        $product = Product::factory()->create();
         $order = Order::factory()->create([
-            'order_status_id' => Order::STATUS_READY_TO_MOUNT,
+            'order_status_id' => Order::STATUS_IN_MOUNT,
         ]);
-
-        // Create SKU with MAF assigned
         ProductSKU::factory()->create([
             'order_id' => $order->id,
             'product_id' => $product->id,
-            'maf_order_id' => $mafOrder->id,
+            'maf_order_id' => null,
         ]);
 
         $response = $this->actingAs($this->managerUser)
             ->get(route('order.generate-installation-pack', $order));
 
         $response->assertRedirect(route('order.show', $order));
-        $response->assertSessionHas('success');
+        $response->assertSessionHas('danger');
+        Bus::assertNotDispatched(GenerateInstallationPack::class);
     }
 
     // ==================== Export ====================

+ 36 - 0
tests/Feature/ReclamationControllerTest.php

@@ -230,6 +230,24 @@ class ReclamationControllerTest extends TestCase
         $this->assertCount(1, $reclamation->fresh()->photos_before);
     }
 
+    public function test_can_upload_photo_before_in_webp_format(): void
+    {
+        Storage::fake('public');
+
+        $reclamation = Reclamation::factory()->create();
+        $photo = UploadedFile::fake()->create('photo_before.webp', 100, 'image/webp');
+
+        $response = $this->actingAs($this->managerUser)
+            ->post(route('reclamations.upload-photo-before', $reclamation), [
+                'photo' => [$photo],
+            ]);
+
+        $response->assertRedirect();
+        $saved = $reclamation->fresh()->photos_before->first();
+        $this->assertNotNull($saved);
+        $this->assertSame('photo_before.webp', $saved->original_name);
+    }
+
     public function test_upload_photo_before_preserves_unicode_and_quotes_filename(): void
     {
         Storage::fake('public');
@@ -286,6 +304,24 @@ class ReclamationControllerTest extends TestCase
         $this->assertCount(1, $reclamation->fresh()->photos_after);
     }
 
+    public function test_can_upload_photo_after_in_webp_format(): void
+    {
+        Storage::fake('public');
+
+        $reclamation = Reclamation::factory()->create();
+        $photo = UploadedFile::fake()->create('photo_after.webp', 100, 'image/webp');
+
+        $response = $this->actingAs($this->managerUser)
+            ->post(route('reclamations.upload-photo-after', $reclamation), [
+                'photo' => [$photo],
+            ]);
+
+        $response->assertRedirect();
+        $saved = $reclamation->fresh()->photos_after->first();
+        $this->assertNotNull($saved);
+        $this->assertSame('photo_after.webp', $saved->original_name);
+    }
+
     public function test_can_delete_photo_after(): void
     {
         Storage::fake('public');

+ 7 - 1
tests/TestCase.php

@@ -2,9 +2,15 @@
 
 namespace Tests;
 
+use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
 
 abstract class TestCase extends BaseTestCase
 {
-    //
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->withoutMiddleware(ValidateCsrfToken::class);
+    }
 }