diff --git a/assets/styles/modes.css b/assets/styles/modes.css index 33343ca..383815b 100644 --- a/assets/styles/modes.css +++ b/assets/styles/modes.css @@ -306,19 +306,211 @@ /* Enhanced mode styles (for future use) */ [data-website-mode="enhanced"] .btn { - background: linear-gradient(45deg, var(--bs-primary), var(--bs-secondary)); - border: 2px solid var(--bs-primary); - transition: all 0.3s ease; + background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red)); + background-size: 400% 400%; + animation: rainbowGradient 1s ease infinite; + border: 4px solid var(--bs-white); + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + position: relative; + overflow: hidden; + transition: all 0.2s ease; } [data-website-mode="enhanced"] .btn:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.2); + animation: rainbowGradient 0.5s ease infinite; + box-shadow: 0 0 30px var(--bs-pink), 0 0 60px var(--bs-purple); +} + +[data-website-mode="enhanced"] .btn::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255,255,255,0.5), transparent); + transform: rotate(45deg); + animation: spin 0.5s linear infinite; } [data-website-mode="enhanced"] .navbar { - background: linear-gradient(90deg, var(--bs-primary), var(--bs-secondary)); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); + background: linear-gradient(90deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red)); + background-size: 200% 200%; + animation: rainbowGradient 2s ease infinite; + box-shadow: 0 0 50px rgba(255, 105, 180, 0.9); + height: auto !important; + min-height: 56px; +} + +[data-website-mode="enhanced"] .navbar-brand { + animation: rainbowText 0.8s infinite; + font-size: 1.8em; + text-shadow: 3px 3px 6px rgba(0,0,0,0.5); + position: relative; + overflow: hidden; +} + +[data-website-mode="enhanced"] .navbar-brand::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255,255,255,0.3), transparent); + transform: rotate(45deg); + animation: spin 2s linear infinite; +} + +[data-website-mode="enhanced"] .navbar-nav .nav-link { + animation: rainbowText 1.2s infinite; + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + border: 2px solid transparent; + border-radius: 8px; + padding: 8px 16px; + margin: 0 4px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +[data-website-mode="enhanced"] .navbar-nav .nav-link::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + animation: shine 1.5s ease-in-out infinite; +} + +[data-website-mode="enhanced"] .navbar-nav .nav-link:hover { + background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple)); + border-color: var(--bs-white); + box-shadow: 0 0 20px var(--bs-pink); +} + +[data-website-mode="enhanced"] .navbar-nav .nav-link.active { + background: linear-gradient(45deg, var(--bs-yellow), var(--bs-orange)); + border-color: var(--bs-white); + box-shadow: 0 0 25px var(--bs-yellow); +} + +[data-website-mode="enhanced"] .navbar-text { + animation: rainbowText 1.5s infinite; + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0,0,0,0.5); + border: 2px solid var(--bs-white); + border-radius: 8px; + padding: 6px 12px; + background: linear-gradient(45deg, var(--bs-cyan), var(--bs-blue)); + box-shadow: 0 0 15px var(--bs-cyan); +} + +[data-website-mode="enhanced"] .navbar-toggler { + border: 3px solid var(--bs-white); + background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple)); + animation: rainbowGradient 0.6s ease infinite; + box-shadow: 0 0 20px var(--bs-pink); +} + +[data-website-mode="enhanced"] .navbar-toggler:focus { + box-shadow: 0 0 30px var(--bs-pink), 0 0 0 0.2rem rgba(255, 105, 180, 0.5); +} + +[data-website-mode="enhanced"] .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + animation: spin 1s linear infinite; +} + +[data-website-mode="enhanced"] .dropdown-menu { + background: linear-gradient(135deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan)); + border: 3px solid var(--bs-white); + box-shadow: 0 0 30px rgba(255,105,180,0.8); + animation: rainbowGradient 2s ease infinite; +} + +[data-website-mode="enhanced"] .dropdown-item { + animation: rainbowText 1.8s infinite; + font-weight: bold; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); + border-bottom: 1px solid rgba(255,255,255,0.3); + transition: all 0.3s ease; +} + +[data-website-mode="enhanced"] .dropdown-item:hover { + background: linear-gradient(45deg, var(--bs-yellow), var(--bs-orange)); + color: var(--bs-white); + box-shadow: 0 0 15px var(--bs-yellow); +} + +[data-website-mode="enhanced"] .navbar-collapse { + background: linear-gradient(135deg, rgba(255,105,180,0.1), rgba(138,43,226,0.1)); + border-radius: 8px; + margin-top: 8px; + padding: 8px; + border: 2px solid var(--bs-pink); +} + +[data-website-mode="enhanced"] h1, [data-website-mode="enhanced"] h2, [data-website-mode="enhanced"] h3 { + animation: rainbowText 1.5s infinite; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +[data-website-mode="enhanced"] .table { + background: linear-gradient(135deg, rgba(255,105,180,0.2), rgba(138,43,226,0.2), rgba(0,255,255,0.2)); + animation: rainbowGradient 3s ease infinite; + border: 3px solid var(--bs-pink); + box-shadow: 0 0 30px rgba(255,105,180,0.5); +} + +[data-website-mode="enhanced"] .table th { + background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple)); + color: var(--bs-white); + animation: rainbowGradient 0.8s ease infinite; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); + font-size: 1.1em; +} + +[data-website-mode="enhanced"] .form-control { + border: 3px solid var(--bs-pink); + box-shadow: 0 0 15px var(--bs-pink); +} + +[data-website-mode="enhanced"] .alert { + animation: rainbowGradient 0.6s ease infinite; + border: 4px solid var(--bs-white); + font-weight: bold; + font-size: 1.1em; +} + +[data-website-mode="enhanced"] .card { + background: linear-gradient(45deg, rgba(255,105,180,0.2), rgba(138,43,226,0.2)); + border: 3px solid var(--bs-purple); + box-shadow: 0 0 35px rgba(138,43,226,0.6); +} + +[data-website-mode="enhanced"] .modal-content { + background: linear-gradient(135deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan)); + border: 4px solid var(--bs-white); + box-shadow: 0 0 50px rgba(255,105,180,0.8); + animation: rainbowGradient 2s ease infinite; +} + +[data-website-mode="enhanced"] .modal-header { + background: linear-gradient(90deg, var(--bs-yellow), var(--bs-orange)); + animation: rainbowGradient 0.8s ease infinite; + font-size: 1.2em; +} + +[data-website-mode="enhanced"] .number-input-wrapper { +} + +[data-website-mode="enhanced"] .number-input-wrapper .btn { + animation: rainbowGradient 0.3s ease infinite; } /* Emoji Footprint Animation */ diff --git a/composer.json b/composer.json index 8e10aae..77a6ce6 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.14", "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", + "runtime/frankenphp-symfony": "^0.2.0", "symfony/asset": "7.3.*", "symfony/asset-mapper": "7.3.*", "symfony/console": "7.3.*", diff --git a/composer.lock b/composer.lock index 6218895..50db31c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c4121f65f6a9cee00e98a29d74d53e2a", + "content-hash": "0171698e06036913d5ba729e28df03c7", "packages": [ { "name": "composer/semver", @@ -1562,6 +1562,58 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "runtime/frankenphp-symfony", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-runtime/frankenphp-symfony.git", + "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/56822c3631d9522a3136a4c33082d006bdfe4bad", + "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/runtime": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Runtime\\FrankenPhpSymfony\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.dev" + } + ], + "description": "FrankenPHP runtime for Symfony", + "support": { + "issues": "https://github.com/php-runtime/frankenphp-symfony/issues", + "source": "https://github.com/php-runtime/frankenphp-symfony/tree/0.2.0" + }, + "funding": [ + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-12-12T12:06:11+00:00" + }, { "name": "symfony/asset", "version": "v7.3.0", diff --git a/src/Controller/DrinkTypeBulkController.php b/src/Controller/DrinkTypeBulkController.php index 855d460..d1ebcdb 100644 --- a/src/Controller/DrinkTypeBulkController.php +++ b/src/Controller/DrinkTypeBulkController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller; use App\Form\BulkEditDrinkTypeStockForm; +use App\Form\BulkEditDrinkTypeWantedStockForm; use App\Repository\DrinkTypeRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -38,7 +39,7 @@ final class DrinkTypeBulkController extends AbstractController $entityManager->flush(); - $this->addFlash('success', 'Wanted stock levels updated successfully!'); + $this->addFlash('success', 'Current stock levels updated successfully!'); return $this->redirectToRoute('app_drink_type_bulk_edit_stock'); } @@ -48,4 +49,38 @@ final class DrinkTypeBulkController extends AbstractController 'drinkTypes' => $drinkTypes, ]); } + + #[Route('/bulk-edit-wanted-stock', name: 'app_drink_type_bulk_edit_wanted_stock')] + public function bulkEditWantedStock( + Request $request, + DrinkTypeRepository $drinkTypeRepository, + EntityManagerInterface $entityManager + ): Response { + $drinkTypes = $drinkTypeRepository->findAll(); + + $form = $this->createForm(BulkEditDrinkTypeWantedStockForm::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_wanted_stock'); + } + + return $this->render('drink_type/bulk_edit_wanted_stock.html.twig', [ + 'form' => $form->createView(), + 'drinkTypes' => $drinkTypes, + ]); + } } diff --git a/src/Form/BulkEditDrinkTypeStockForm.php b/src/Form/BulkEditDrinkTypeStockForm.php index 6d3962f..1a21016 100644 --- a/src/Form/BulkEditDrinkTypeStockForm.php +++ b/src/Form/BulkEditDrinkTypeStockForm.php @@ -25,7 +25,7 @@ class BulkEditDrinkTypeStockForm extends AbstractType 'by_reference' => false, ]) ->add('save', SubmitType::class, [ - 'label' => 'Update Wanted Stock Levels', + 'label' => 'Update Current Stock Levels', 'attr' => [ 'class' => 'btn btn-primary', ], diff --git a/src/Form/BulkEditDrinkTypeWantedStockForm.php b/src/Form/BulkEditDrinkTypeWantedStockForm.php new file mode 100644 index 0000000..b692bad --- /dev/null +++ b/src/Form/BulkEditDrinkTypeWantedStockForm.php @@ -0,0 +1,41 @@ +add('drinkTypes', CollectionType::class, [ + 'entry_type' => DrinkTypeWantedStockEditType::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 index 21489c4..0b78796 100644 --- a/src/Form/DrinkTypeStockEditType.php +++ b/src/Form/DrinkTypeStockEditType.php @@ -11,6 +11,8 @@ use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; +use Symfony\Component\Validator\Constraints\NotBlank; class DrinkTypeStockEditType extends AbstractType { @@ -26,13 +28,22 @@ class DrinkTypeStockEditType extends AbstractType 'readonly' => true, ], ]) - ->add('wantedStock', NumberType::class, [ - 'label' => 'Wanted Stock', + ->add('currentStock', NumberType::class, [ + 'label' => 'Current Stock', 'attr' => [ 'min' => 0, 'step' => 1, 'class' => 'form-control', ], + 'constraints' => [ + new NotBlank([ + 'message' => 'Current stock cannot be blank', + ]), + new GreaterThanOrEqual([ + 'value' => 0, + 'message' => 'Current stock must not be negative', + ]), + ], ]); } diff --git a/src/Form/DrinkTypeWantedStockEditType.php b/src/Form/DrinkTypeWantedStockEditType.php new file mode 100644 index 0000000..d3c7dfc --- /dev/null +++ b/src/Form/DrinkTypeWantedStockEditType.php @@ -0,0 +1,56 @@ +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', + ], + 'constraints' => [ + new NotBlank([ + 'message' => 'Wanted stock cannot be blank', + ]), + new GreaterThanOrEqual([ + 'value' => 0, + 'message' => 'Wanted stock must not be negative', + ]), + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => DrinkType::class, + ]); + } +} diff --git a/src/Service/DrinkType/FilterLowStockDrinks.php b/src/Service/DrinkType/FilterLowStockDrinks.php new file mode 100644 index 0000000..d52da3f --- /dev/null +++ b/src/Service/DrinkType/FilterLowStockDrinks.php @@ -0,0 +1,24 @@ + + $drinkType->getCurrentStock() < ($drinkType->getWantedStock() * $this->lowStockMultiplier->getValue()) + ); + } + +} diff --git a/templates/drink_type/bulk_edit_stock.html.twig b/templates/drink_type/bulk_edit_stock.html.twig index 01454f9..f63a43c 100644 --- a/templates/drink_type/bulk_edit_stock.html.twig +++ b/templates/drink_type/bulk_edit_stock.html.twig @@ -1,10 +1,10 @@ {% extends 'base.html.twig' %} -{% block title %}Bulk Edit Drink Type Wanted Stock{% endblock %} +{% block title %}Bulk Edit Drink Type Current Stock{% endblock %} {% block body %}
-

Bulk Edit Drink Type Wanted Stock

+

Bulk Edit Drink Type Current Stock

{% for message in app.flashes('success') %}
diff --git a/tests/Feature/Web/BulkEditFormTest.php b/tests/Feature/Web/BulkEditFormTest.php index a878829..82c6b40 100644 --- a/tests/Feature/Web/BulkEditFormTest.php +++ b/tests/Feature/Web/BulkEditFormTest.php @@ -33,20 +33,20 @@ test('Bulk Edit Form displays correctly with drink types', function (): void { $this->assertResponseIsSuccessful(); // Check page title - $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Wanted Stock'); + $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Current 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'); + $this->assertSelectorTextContains('button[type="submit"]', 'Update Current Stock Levels'); // Check that input fields exist for each drink type - $this->assertCount(2, $crawler->filter('input[name*="[wantedStock]"]')); + $this->assertCount(2, $crawler->filter('input[name*="[currentStock]"]')); }); -test('Bulk Edit Form submission updates drink type wanted stock levels', function (): void { +test('Bulk Edit Form submission updates drink type current stock levels', function (): void { $this->ensureKernelShutdown(); $client = static::createClient(); @@ -71,12 +71,12 @@ test('Bulk Edit Form submission updates drink type wanted stock levels', functio $crawler = $client->request('GET', '/drink-types/bulk-edit-stock'); // Submit the form with updated values - $form = $crawler->selectButton('Update Wanted Stock Levels')->form(); + $form = $crawler->selectButton('Update Current Stock Levels')->form(); - // Update the wanted stock values - note: drink types are ordered by wantedStock DESC + // Update the current 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 + $form['bulk_edit_drink_type_stock_form[drinkTypes][0][currentStock]'] = 25; // Beer + $form['bulk_edit_drink_type_stock_form[drinkTypes][1][currentStock]'] = 15; // Cola $client->submit($form); @@ -87,7 +87,7 @@ test('Bulk Edit Form submission updates drink type wanted stock levels', functio $client->followRedirect(); // Check for success message - $this->assertSelectorTextContains('.alert-success', 'Wanted stock levels updated successfully!'); + $this->assertSelectorTextContains('.alert-success', 'Current stock levels updated successfully!'); // Verify the database was updated $em->clear(); // Clear entity manager to reload from database @@ -95,8 +95,8 @@ test('Bulk Edit Form submission updates drink type wanted stock levels', functio $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 + expect($updatedDrinkType1->getCurrentStock())->toBe(15); // Cola + expect($updatedDrinkType2->getCurrentStock())->toBe(25); // Beer }); test('Bulk Edit Form handles empty drink types list', function (): void { @@ -119,14 +119,14 @@ test('Bulk Edit Form handles empty drink types list', function (): void { $this->assertResponseIsSuccessful(); // Check page title - $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Wanted Stock'); + $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Current 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'); + $this->assertSelectorTextContains('button[type="submit"]', 'Update Current Stock Levels'); }); test('Bulk Edit Form rejects negative values (validation)', function (): void { @@ -148,20 +148,20 @@ test('Bulk Edit Form rejects negative values (validation)', function (): void { $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; + $form = $crawler->selectButton('Update Current Stock Levels')->form(); + $form['bulk_edit_drink_type_stock_form[drinkTypes][0][currentStock]'] = -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'); + $this->assertSelectorTextContains('h1', 'Bulk Edit Drink Type Current Stock'); + $this->assertSelectorTextContains('.form-error-message, .invalid-feedback', 'Current 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 + expect($updatedDrinkType->getCurrentStock())->toBe(5); // Should remain unchanged }); test('Bulk Edit Form preserves drink type names as read-only', function (): void { @@ -188,8 +188,8 @@ test('Bulk Edit Form preserves drink type names as read-only', function (): void 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; + $form = $crawler->selectButton('Update Current Stock Levels')->form(); + $form['bulk_edit_drink_type_stock_form[drinkTypes][0][currentStock]'] = 15; $client->submit($form); @@ -200,5 +200,5 @@ test('Bulk Edit Form preserves drink type names as read-only', function (): void $em->clear(); $updatedDrinkType = $em->find(DrinkType::class, $drinkType->getId()); expect($updatedDrinkType->getName())->toBe('Original Name'); - expect($updatedDrinkType->getWantedStock())->toBe(15); + expect($updatedDrinkType->getCurrentStock())->toBe(15); }); diff --git a/tests/Feature/Web/BulkEditWantedStockFormTest.php b/tests/Feature/Web/BulkEditWantedStockFormTest.php new file mode 100644 index 0000000..9677fc3 --- /dev/null +++ b/tests/Feature/Web/BulkEditWantedStockFormTest.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-wanted-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 Wanted Stock 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-wanted-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_wanted_stock_form[drinkTypes][0][wantedStock]'] = 25; // Beer + $form['bulk_edit_drink_type_wanted_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-wanted-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 Wanted Stock 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-wanted-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 Wanted Stock 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-wanted-stock'); + + // Submit the form with negative value + $form = $crawler->selectButton('Update Wanted Stock Levels')->form(); + $form['bulk_edit_drink_type_wanted_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 Wanted Stock 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-wanted-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_wanted_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); +});