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') %}
+
+ {{ message }}
+
+
+ {% endfor %}
+
+ {{ form_start(form) }}
+
+
+
+
+
+ Drink Type |
+ Wanted Stock |
+
+
+
+ {% for drinkTypeForm in form.drinkTypes %}
+
+
+ {{ form_widget(drinkTypeForm.id) }}
+ {{ form_widget(drinkTypeForm.name) }}
+ |
+
+ {{ form_widget(drinkTypeForm.wantedStock) }}
+ {{ form_errors(drinkTypeForm.wantedStock) }}
+ |
+
+ {% endfor %}
+
+
+
+
+
+ {{ 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
-
-
-
-
- Id |
- Quantity |
- Timestamp |
- CreatedAt |
- UpdatedAt |
- actions |
-
-
-
- {% for inventory_record in inventory_records %}
-
- {{ 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
- |
-
- {% else %}
-
- no records found |
-
- {% endfor %}
-
-
-
- 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);
+});