vibe
This commit is contained in:
parent
16533b1495
commit
ab0677c463
14 changed files with 705 additions and 38 deletions
|
@ -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 */
|
||||
|
|
|
@ -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
54
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
41
src/Form/BulkEditDrinkTypeWantedStockForm.php
Normal file
41
src/Form/BulkEditDrinkTypeWantedStockForm.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
56
src/Form/DrinkTypeWantedStockEditType.php
Normal file
56
src/Form/DrinkTypeWantedStockEditType.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
24
src/Service/DrinkType/FilterLowStockDrinks.php
Normal file
24
src/Service/DrinkType/FilterLowStockDrinks.php
Normal 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())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 %}
|
||||
|
|
49
templates/drink_type/bulk_edit_wanted_stock.html.twig
Normal file
49
templates/drink_type/bulk_edit_wanted_stock.html.twig
Normal 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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
204
tests/Feature/Web/BulkEditWantedStockFormTest.php
Normal file
204
tests/Feature/Web/BulkEditWantedStockFormTest.php
Normal 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);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue