This commit is contained in:
lubiana 2025-06-14 18:32:57 +02:00
parent 16533b1495
commit ab0677c463
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
14 changed files with 705 additions and 38 deletions

View file

@ -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 */

View file

@ -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.*",

54
composer.lock generated
View file

@ -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",

View file

@ -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,
]);
}
}

View file

@ -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',
],

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BulkEditDrinkTypeWantedStockForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->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,
]);
}
}

View file

@ -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',
]),
],
]);
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\DrinkType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
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 DrinkTypeWantedStockEditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->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,
]);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Service\DrinkType;
use App\Entity\DrinkType;
use App\Service\Config\LowStockMultiplier;
final readonly class FilterLowStockDrinks
{
public function __construct(private LowStockMultiplier $lowStockMultiplier) {}
/** @param DrinkType[] $drinkTypes */
public function __invoke(array $drinkTypes): array
{
return array_filter(
$drinkTypes,
fn(DrinkType $drinkType): bool =>
$drinkType->getCurrentStock() < ($drinkType->getWantedStock() * $this->lowStockMultiplier->getValue())
);
}
}

View file

@ -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 %}
<div class="container mt-4">
<h1>Bulk Edit Drink Type Wanted Stock</h1>
<h1>Bulk Edit Drink Type Current Stock</h1>
{% for message in app.flashes('success') %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
@ -20,7 +20,7 @@
<thead>
<tr>
<th>Drink Type</th>
<th>Wanted Stock</th>
<th>Current Stock</th>
</tr>
</thead>
<tbody>
@ -31,8 +31,8 @@
{{ form_widget(drinkTypeForm.name) }}
</td>
<td>
{{ form_widget(drinkTypeForm.wantedStock) }}
{{ form_errors(drinkTypeForm.wantedStock) }}
{{ form_widget(drinkTypeForm.currentStock) }}
{{ form_errors(drinkTypeForm.currentStock) }}
</td>
</tr>
{% endfor %}

View file

@ -0,0 +1,49 @@
{% extends 'base.html.twig' %}
{% block title %}Bulk Edit Drink Type Wanted Stock{% endblock %}
{% block body %}
<div class="container mt-4">
<h1>Bulk Edit Drink Type Wanted Stock</h1>
{% for message in app.flashes('success') %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{{ form_start(form) }}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Drink Type</th>
<th>Wanted Stock</th>
</tr>
</thead>
<tbody>
{% for drinkTypeForm in form.drinkTypes %}
<tr>
<td>
{{ form_widget(drinkTypeForm.id) }}
{{ form_widget(drinkTypeForm.name) }}
</td>
<td>
{{ form_widget(drinkTypeForm.wantedStock) }}
{{ form_errors(drinkTypeForm.wantedStock) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
{{ form_widget(form.save) }}
</div>
{{ form_end(form) }}
</div>
{% endblock %}

View file

@ -37,6 +37,8 @@
</table>
<div class="mt-3">
<a href="{{ path('app_drink_type_index') }}" class="btn btn-primary">View All Drinks</a>
<a href="{{ path('app_drink_type_bulk_edit_stock') }}" class="btn btn-secondary">Mass Update Current Stock</a>
<a href="{{ path('app_drink_type_bulk_edit_wanted_stock') }}" class="btn btn-secondary">Mass Update Wanted Stock</a>
</div>
</div>
</div>

View file

@ -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);
});

View file

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use Doctrine\ORM\EntityManagerInterface;
test('Bulk Edit Wanted Stock Form displays correctly with drink types', 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();
// 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);
});