diff --git a/src/Controller/DrinkTypeBulkController.php b/src/Controller/DrinkTypeBulkController.php new file mode 100644 index 0000000..855d460 --- /dev/null +++ b/src/Controller/DrinkTypeBulkController.php @@ -0,0 +1,51 @@ +findAll(); + + $form = $this->createForm(BulkEditDrinkTypeStockForm::class, [ + 'drinkTypes' => $drinkTypes, + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + foreach ($data['drinkTypes'] as $drinkType) { + $entityManager->persist($drinkType); + } + + $entityManager->flush(); + + $this->addFlash('success', 'Wanted stock levels updated successfully!'); + + return $this->redirectToRoute('app_drink_type_bulk_edit_stock'); + } + + return $this->render('drink_type/bulk_edit_stock.html.twig', [ + 'form' => $form->createView(), + 'drinkTypes' => $drinkTypes, + ]); + } +} diff --git a/src/Controller/DrinkTypeController.php b/src/Controller/DrinkTypeController.php index 4b097ed..714e92d 100644 --- a/src/Controller/DrinkTypeController.php +++ b/src/Controller/DrinkTypeController.php @@ -8,10 +8,8 @@ use App\Entity\DrinkType; use App\Form\DrinkTypeForm; use App\Form\DrinkTypeFormCurrentStockForm; use App\Repository\DrinkTypeRepository; -use App\Repository\PropertyChangeLogRepository; use App\Service\DrinkType\GetStockHistory; use App\Service\DrinkType\GetWantedHistory; -use App\Service\InventoryService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -54,8 +52,7 @@ final class DrinkTypeController extends AbstractController DrinkType $drinkType, GetStockHistory $getStockHistory, GetWantedHistory $getWantedHistory, - ): Response - { + ): Response { // Get orders that contain this drink type $orderItems = $drinkType->getOrderItems(); diff --git a/src/Entity/DrinkType.php b/src/Entity/DrinkType.php index 7d76553..4b1694e 100644 --- a/src/Entity/DrinkType.php +++ b/src/Entity/DrinkType.php @@ -9,6 +9,7 @@ use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: DrinkTypeRepository::class)] #[ORM\Table(name: 'drink_type')] @@ -33,6 +34,7 @@ class DrinkType #[ORM\Column(type: 'text', nullable: true)] private null|string $description = null; #[ORM\Column(type: 'integer')] + #[Assert\GreaterThanOrEqual(0, message: 'Wanted stock must not be negative.')] private int $wantedStock = 10; #[ORM\Column(type: 'integer')] diff --git a/src/Form/BulkEditDrinkTypeStockForm.php b/src/Form/BulkEditDrinkTypeStockForm.php new file mode 100644 index 0000000..6d3962f --- /dev/null +++ b/src/Form/BulkEditDrinkTypeStockForm.php @@ -0,0 +1,41 @@ +add('drinkTypes', CollectionType::class, [ + 'entry_type' => DrinkTypeStockEditType::class, + 'entry_options' => [ + 'label' => false, + ], + 'allow_add' => false, + 'allow_delete' => false, + 'by_reference' => false, + ]) + ->add('save', SubmitType::class, [ + 'label' => 'Update Wanted Stock Levels', + 'attr' => [ + 'class' => 'btn btn-primary', + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => null, + ]); + } +} diff --git a/src/Form/DrinkTypeStockEditType.php b/src/Form/DrinkTypeStockEditType.php new file mode 100644 index 0000000..21489c4 --- /dev/null +++ b/src/Form/DrinkTypeStockEditType.php @@ -0,0 +1,45 @@ +add('id', HiddenType::class, [ + 'mapped' => false, + ]) + ->add('name', TextType::class, [ + 'disabled' => true, + 'attr' => [ + 'readonly' => true, + ], + ]) + ->add('wantedStock', NumberType::class, [ + 'label' => 'Wanted Stock', + 'attr' => [ + 'min' => 0, + 'step' => 1, + 'class' => 'form-control', + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => DrinkType::class, + ]); + } +} diff --git a/src/Service/DrinkType/CreateNewOrderItems.php b/src/Service/DrinkType/CreateNewOrderItems.php index 6dfb4fa..39c4397 100644 --- a/src/Service/DrinkType/CreateNewOrderItems.php +++ b/src/Service/DrinkType/CreateNewOrderItems.php @@ -19,7 +19,7 @@ final readonly class CreateNewOrderItems public function __invoke(Order $order): void { new ArrayCollection($this->drinkTypeRepository->findWanted())->forAll( - function (int $index, DrinkType $drinkType) use ($order) { + function (int $index, DrinkType $drinkType) use ($order): true { $quantity = $drinkType->getWantedStock() - $drinkType->getCurrentStock(); if ($quantity > 0) { $order->addOrderItem( diff --git a/templates/drink_type/bulk_edit_stock.html.twig b/templates/drink_type/bulk_edit_stock.html.twig new file mode 100644 index 0000000..01454f9 --- /dev/null +++ b/templates/drink_type/bulk_edit_stock.html.twig @@ -0,0 +1,49 @@ +{% extends 'base.html.twig' %} + +{% block title %}Bulk Edit Drink Type Wanted Stock{% endblock %} + +{% block body %} +
+

Bulk Edit Drink Type Wanted Stock

+ + {% for message in app.flashes('success') %} + + {% endfor %} + + {{ form_start(form) }} + +
+ + + + + + + + + {% for drinkTypeForm in form.drinkTypes %} + + + + + {% endfor %} + +
Drink TypeWanted Stock
+ {{ form_widget(drinkTypeForm.id) }} + {{ form_widget(drinkTypeForm.name) }} + + {{ form_widget(drinkTypeForm.wantedStock) }} + {{ form_errors(drinkTypeForm.wantedStock) }} +
+
+ +
+ {{ form_widget(form.save) }} +
+ + {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/inventory_record/_delete_form.html.twig b/templates/inventory_record/_delete_form.html.twig deleted file mode 100644 index 6c5c5fd..0000000 --- a/templates/inventory_record/_delete_form.html.twig +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
diff --git a/templates/inventory_record/_form.html.twig b/templates/inventory_record/_form.html.twig deleted file mode 100644 index bf20b98..0000000 --- a/templates/inventory_record/_form.html.twig +++ /dev/null @@ -1,4 +0,0 @@ -{{ form_start(form) }} - {{ form_widget(form) }} - -{{ form_end(form) }} diff --git a/templates/inventory_record/_modal_form.html.twig b/templates/inventory_record/_modal_form.html.twig deleted file mode 100644 index fd4b651..0000000 --- a/templates/inventory_record/_modal_form.html.twig +++ /dev/null @@ -1,7 +0,0 @@ -{{ form_start(form, {'attr': {'hx-post': form.vars.action, 'hx-target': '#htmxModalBody'}}) }} -{{ form_widget(form) }} - -{{ form_end(form) }} diff --git a/templates/inventory_record/edit.html.twig b/templates/inventory_record/edit.html.twig deleted file mode 100644 index f41339b..0000000 --- a/templates/inventory_record/edit.html.twig +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Edit InventoryRecord{% endblock %} - -{% block body %} -

Edit InventoryRecord

- - {{ include('inventory_record/_form.html.twig', {'button_label': 'Update'}) }} - - back to list - - {{ include('inventory_record/_delete_form.html.twig') }} -{% endblock %} diff --git a/templates/inventory_record/index.html.twig b/templates/inventory_record/index.html.twig deleted file mode 100644 index 7024ab2..0000000 --- a/templates/inventory_record/index.html.twig +++ /dev/null @@ -1,41 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}InventoryRecord index{% endblock %} - -{% block body %} -

InventoryRecord index

- - - - - - - - - - - - - - {% for inventory_record in inventory_records %} - - - - - - - - - {% else %} - - - - {% endfor %} - -
IdQuantityTimestampCreatedAtUpdatedAtactions
{{ inventory_record.id }}{{ inventory_record.quantity }}{{ inventory_record.timestamp ? inventory_record.timestamp|date('Y-m-d H:i:s') : '' }}{{ inventory_record.createdAt ? inventory_record.createdAt|date('Y-m-d H:i:s') : '' }}{{ inventory_record.updatedAt ? inventory_record.updatedAt|date('Y-m-d H:i:s') : '' }} - show - edit -
no records found
- - Create new -{% endblock %} diff --git a/templates/inventory_record/new.html.twig b/templates/inventory_record/new.html.twig deleted file mode 100644 index 3ae76a3..0000000 --- a/templates/inventory_record/new.html.twig +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}New InventoryRecord{% endblock %} - -{% block body %} -

Create new InventoryRecord

- - {{ include('inventory_record/_form.html.twig') }} - - back to list -{% endblock %} diff --git a/templates/inventory_record/show.html.twig b/templates/inventory_record/show.html.twig deleted file mode 100644 index 2caf816..0000000 --- a/templates/inventory_record/show.html.twig +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}InventoryRecord{% endblock %} - -{% block body %} -

InventoryRecord

- - - - - - - - - - - - - - - - - - - - - - - - -
Id{{ inventory_record.id }}
Quantity{{ inventory_record.quantity }}
Timestamp{{ inventory_record.timestamp ? inventory_record.timestamp|date('Y-m-d H:i:s') : '' }}
CreatedAt{{ inventory_record.createdAt ? inventory_record.createdAt|date('Y-m-d H:i:s') : '' }}
UpdatedAt{{ inventory_record.updatedAt ? inventory_record.updatedAt|date('Y-m-d H:i:s') : '' }}
- - back to list - - edit - - {{ include('inventory_record/_delete_form.html.twig') }} -{% endblock %} diff --git a/tests/Feature/Service/DrinkType/CreateNewOrderItemsTest.php b/tests/Feature/Service/DrinkType/CreateNewOrderItemsTest.php index 700591d..89581c2 100644 --- a/tests/Feature/Service/DrinkType/CreateNewOrderItemsTest.php +++ b/tests/Feature/Service/DrinkType/CreateNewOrderItemsTest.php @@ -84,7 +84,7 @@ test('CreateNewOrderItems adds order items for wanted drink types', function (): expect($orderItem2->getQuantity())->toBe(6); // 8 wanted - 2 current = 6 to order // Verify that drink types 3 and 4 don't have order items - $drinkType3Names = array_map(fn($item) => $item->getDrinkType()->getName(), $orderItems->toArray()); + $drinkType3Names = array_map(fn($item): string => $item->getDrinkType()->getName(), $orderItems->toArray()); expect($drinkType3Names)->not->toContain('Drink Type 3'); expect($drinkType3Names)->not->toContain('Drink Type 4'); }); diff --git a/tests/Feature/Web/BulkEditFormTest.php b/tests/Feature/Web/BulkEditFormTest.php new file mode 100644 index 0000000..a878829 --- /dev/null +++ b/tests/Feature/Web/BulkEditFormTest.php @@ -0,0 +1,204 @@ +ensureKernelShutdown(); + $client = static::createClient(); + + // Create test drink types + $em = $this->getContainer()->get(EntityManagerInterface::class); + + $drinkType1 = new DrinkType(); + $drinkType1->setName('Cola'); + $drinkType1->setWantedStock(10); + $drinkType1->setCurrentStock(5); + + $drinkType2 = new DrinkType(); + $drinkType2->setName('Beer'); + $drinkType2->setWantedStock(20); + $drinkType2->setCurrentStock(15); + + $em->persist($drinkType1); + $em->persist($drinkType2); + $em->flush(); + + // Request the bulk edit page + $crawler = $client->request('GET', '/drink-types/bulk-edit-stock'); + + // Validate successful response + $this->assertResponseIsSuccessful(); + + // Check page title + $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Wanted Stock'); + + // Check that drink types are displayed in the table (they're in the value attribute of disabled inputs) + $this->assertSelectorExists('input[value="Cola"]'); + $this->assertSelectorExists('input[value="Beer"]'); + + // Check that the form has the correct submit button + $this->assertSelectorTextContains('button[type="submit"]', 'Update Wanted Stock Levels'); + + // Check that input fields exist for each drink type + $this->assertCount(2, $crawler->filter('input[name*="[wantedStock]"]')); +}); + +test('Bulk Edit Form submission updates drink type wanted stock levels', function (): void { + $this->ensureKernelShutdown(); + $client = static::createClient(); + + // Create test drink types + $em = $this->getContainer()->get(EntityManagerInterface::class); + + $drinkType1 = new DrinkType(); + $drinkType1->setName('Cola'); + $drinkType1->setWantedStock(10); + $drinkType1->setCurrentStock(5); + + $drinkType2 = new DrinkType(); + $drinkType2->setName('Beer'); + $drinkType2->setWantedStock(20); + $drinkType2->setCurrentStock(15); + + $em->persist($drinkType1); + $em->persist($drinkType2); + $em->flush(); + + // Get the bulk edit page to get the form + $crawler = $client->request('GET', '/drink-types/bulk-edit-stock'); + + // Submit the form with updated values + $form = $crawler->selectButton('Update Wanted Stock Levels')->form(); + + // Update the wanted stock values - note: drink types are ordered by wantedStock DESC + // So Beer (wantedStock 20) comes first [0], Cola (wantedStock 10) comes second [1] + $form['bulk_edit_drink_type_stock_form[drinkTypes][0][wantedStock]'] = 25; // Beer + $form['bulk_edit_drink_type_stock_form[drinkTypes][1][wantedStock]'] = 15; // Cola + + $client->submit($form); + + // Check that we're redirected back to the same page + $this->assertResponseRedirects('/drink-types/bulk-edit-stock'); + + // Follow the redirect + $client->followRedirect(); + + // Check for success message + $this->assertSelectorTextContains('.alert-success', 'Wanted stock levels updated successfully!'); + + // Verify the database was updated + $em->clear(); // Clear entity manager to reload from database + + $updatedDrinkType1 = $em->find(DrinkType::class, $drinkType1->getId()); + $updatedDrinkType2 = $em->find(DrinkType::class, $drinkType2->getId()); + + expect($updatedDrinkType1->getWantedStock())->toBe(15); // Cola + expect($updatedDrinkType2->getWantedStock())->toBe(25); // Beer +}); + +test('Bulk Edit Form handles empty drink types list', function (): void { + $this->ensureKernelShutdown(); + $client = static::createClient(); + + // Clear existing drink types + $em = $this->getContainer()->get(EntityManagerInterface::class); + $drinkTypeRepository = $em->getRepository(DrinkType::class); + $existingDrinkTypes = $drinkTypeRepository->findAll(); + foreach ($existingDrinkTypes as $drinkType) { + $em->remove($drinkType); + } + $em->flush(); + + // Request the bulk edit page + $crawler = $client->request('GET', '/drink-types/bulk-edit-stock'); + + // Validate successful response + $this->assertResponseIsSuccessful(); + + // Check page title + $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Wanted Stock'); + + // Check that the table exists but has no rows (no tbody content) + $this->assertSelectorExists('table'); + $this->assertCount(0, $crawler->filter('table tbody tr')); + + // Check that the form still has the submit button + $this->assertSelectorTextContains('button[type="submit"]', 'Update Wanted Stock Levels'); +}); + +test('Bulk Edit Form rejects negative values (validation)', function (): void { + $this->ensureKernelShutdown(); + $client = static::createClient(); + + // Create test drink type + $em = $this->getContainer()->get(EntityManagerInterface::class); + + $drinkType = new DrinkType(); + $drinkType->setName('Test Drink'); + $drinkType->setWantedStock(10); + $drinkType->setCurrentStock(5); + + $em->persist($drinkType); + $em->flush(); + + // Get the bulk edit page + $crawler = $client->request('GET', '/drink-types/bulk-edit-stock'); + + // Submit the form with negative value + $form = $crawler->selectButton('Update Wanted Stock Levels')->form(); + $form['bulk_edit_drink_type_stock_form[drinkTypes][0][wantedStock]'] = -5; + + $client->submit($form); + + // The form should NOT redirect, but show the validation error + $this->assertResponseIsSuccessful(); + $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Wanted Stock'); + $this->assertSelectorTextContains('.form-error-message, .invalid-feedback', 'Wanted stock must not be negative'); + + // Verify the database was not updated + $em->clear(); + $updatedDrinkType = $em->find(DrinkType::class, $drinkType->getId()); + expect($updatedDrinkType->getWantedStock())->toBe(10); // Should remain unchanged +}); + +test('Bulk Edit Form preserves drink type names as read-only', function (): void { + $this->ensureKernelShutdown(); + $client = static::createClient(); + + // Create test drink type + $em = $this->getContainer()->get(EntityManagerInterface::class); + + $drinkType = new DrinkType(); + $drinkType->setName('Original Name'); + $drinkType->setWantedStock(10); + $drinkType->setCurrentStock(5); + + $em->persist($drinkType); + $em->flush(); + + // Get the bulk edit page + $crawler = $client->request('GET', '/drink-types/bulk-edit-stock'); + + // Check that the name field is read-only + $nameInput = $crawler->filter('input[name*="[name]"]')->first(); + expect($nameInput->attr('readonly'))->toBe('readonly'); + expect($nameInput->attr('disabled'))->toBe('disabled'); + + // Submit the form + $form = $crawler->selectButton('Update Wanted Stock Levels')->form(); + $form['bulk_edit_drink_type_stock_form[drinkTypes][0][wantedStock]'] = 15; + + $client->submit($form); + + // Follow the redirect + $client->followRedirect(); + + // Verify the name was not changed + $em->clear(); + $updatedDrinkType = $em->find(DrinkType::class, $drinkType->getId()); + expect($updatedDrinkType->getName())->toBe('Original Name'); + expect($updatedDrinkType->getWantedStock())->toBe(15); +});