Compare commits

..

6 Commits

@ -15,4 +15,10 @@ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]
//region FontAwesome //region FontAwesome
import 'fontawesome'; import 'fontawesome';
//endregion //endregion
//region ModalDynamic
import 'modalDynamic'; import 'modalDynamic';
//endregion
//region Forms Collections
import 'formCollection';
//endregion

@ -0,0 +1,31 @@
import 'jqueryLocal';
$(function () {
const machineExtraInfo1 = $('#recipe_edit_form_machineExtraInfo1');
const machineExtraInfo1_div = machineExtraInfo1.parents('div.row').first();
const machineExtraInfo1_label = machineExtraInfo1_div.find('label[for="' + machineExtraInfo1.attr('id') + '"]')
const machineExtraInfo2 = $('#recipe_edit_form_machineExtraInfo2');
const machineExtraInfo2_div = machineExtraInfo2.parents('div.row').first();
const machineExtraInfo2_label = machineExtraInfo2_div.find('label[for="' + machineExtraInfo2.attr('id') + '"]')
$('#recipe_edit_form_machine')
.on('change', function () {
const machine = $(this)
.find(':selected')
.dataDefault(
'machine',
{
labelExtraInfo1: null,
labelExtraInfo2: null,
}
);
machineExtraInfo1_div.toggleClass('d-none', machine.labelExtraInfo1 === null);
machineExtraInfo1_label.text(machine.labelExtraInfo1);
machineExtraInfo2_div.toggleClass('d-none', machine.labelExtraInfo2 === null);
machineExtraInfo2_label.text(machine.labelExtraInfo2);
})
.trigger('change');
});

@ -0,0 +1,40 @@
import 'jqueryLocal';
import {Tooltip} from 'bootstrap';
$(function () {
$(document).on('click', '.collection-add', function (event) {
event.preventDefault();
const button = $(this);
//region Ajoute la ligne
const collectionId = button.data('collection');
const collection = $('#' + collectionId);
const deleteButton = $(`#${collectionId}__collection_entry_add-delete_button > *`);
const rowPrototypePlaceholder = collection.data('prototypePlaceholder');
const rowPrototypeIndex = collection.data('prototypeIndex');
const rowPrototypeRaw = collection.data('prototypeCode').replaceAll(new RegExp(rowPrototypePlaceholder, 'g'), rowPrototypeIndex);
const rowPrototype = $(rowPrototypeRaw);
const rowPrototypeDiv = rowPrototype.find('#' + collectionId + '_' + rowPrototypeIndex);
rowPrototypeDiv.append(deleteButton.clone());
rowPrototype.insertBefore(button.parents('.row').first());
collection.data('prototypeIndex', rowPrototypeIndex + 1);
//endregion
});
$(document).on('click', '.collection-row-delete', function (event) {
event.preventDefault();
const button = $(this);
//region Masque le tooltip (si existant) du bouton, car celui-ci va être supprimé en même temps que sa ligne
const buttonTooltip = Tooltip.getInstance(button);
if (buttonTooltip !== null) {
buttonTooltip.hide();
}
//endregion
//region Supprime la ligne
button.parents('.collection-row').parent().parent().remove();
//endregion
});
});

@ -1,6 +1,6 @@
twig: twig:
file_name_pattern: '*.twig' file_name_pattern: '*.twig'
form_themes: [ 'bootstrap_5_horizontal_layout.html.twig' ] form_themes: [ 'form/my_theme.html.twig' ]
when@test: when@test:
twig: twig:

@ -20,9 +20,16 @@ return [
'path' => './assets/datatables2.js', 'path' => './assets/datatables2.js',
'entrypoint' => true, 'entrypoint' => true,
], ],
'config_recipe_edit' => [
'path' => './assets/config_recipe_edit.js',
'entrypoint' => true,
],
'modalDynamic' => [ 'modalDynamic' => [
'path' => './assets/modalDynamic.js', 'path' => './assets/modalDynamic.js',
], ],
'formCollection' => [
'path' => './assets/formCollection.js',
],
'jqueryLocal' => [ 'jqueryLocal' => [
'path' => './assets/jqueryLocal.js', 'path' => './assets/jqueryLocal.js',
], ],

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250603101350 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP INDEX UNIQ_DA88B1375E237E06 ON recipe
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE recipe DROP name
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE recipe ADD name VARCHAR(50) NOT NULL
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_DA88B1375E237E06 ON recipe (name)
SQL);
}
}

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250606142337 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_77575DA759D8A214E308AC6F ON input_recipe_material (recipe_id, material_id)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_CB0D94B259D8A214E308AC6F ON output_recipe_material (recipe_id, material_id)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP INDEX UNIQ_77575DA759D8A214E308AC6F ON input_recipe_material
SQL);
$this->addSql(<<<'SQL'
DROP INDEX UNIQ_CB0D94B259D8A214E308AC6F ON output_recipe_material
SQL);
}
}

@ -11,12 +11,17 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller for the configuration pages of machines * Controller for the configuration pages of machines
*/ */
#[Route('/Config/Machine')] #[Route('/Config/Machine')]
#[IsGranted('IS_AUTHENTICATED')]
class MachineController extends AbstractController { class MachineController extends AbstractController {
/**
* @var EntityManagerInterface The entity manager
*/
private readonly EntityManagerInterface $entityManager; private readonly EntityManagerInterface $entityManager;
/** /**
* @var MachineRepository The machine repository * @var MachineRepository The machine repository

@ -14,12 +14,17 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller for the configuration pages of material * Controller for the configuration pages of material
*/ */
#[Route('/Config/Material')] #[Route('/Config/Material')]
#[IsGranted('IS_AUTHENTICATED')]
class MaterialController extends AbstractController { class MaterialController extends AbstractController {
/**
* @var EntityManagerInterface The entity manager
*/
private readonly EntityManagerInterface $entityManager; private readonly EntityManagerInterface $entityManager;
/** /**
* @var MaterialRepository The material repository * @var MaterialRepository The material repository

@ -11,12 +11,17 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* Controller for the configuration pages of material types * Controller for the configuration pages of material types
*/ */
#[Route('/Config/MaterialType')] #[Route('/Config/MaterialType')]
#[IsGranted('IS_AUTHENTICATED')]
class MaterialTypeController extends AbstractController { class MaterialTypeController extends AbstractController {
/**
* @var EntityManagerInterface The entity manager
*/
private readonly EntityManagerInterface $entityManager; private readonly EntityManagerInterface $entityManager;
/** /**
* @var MaterialTypeRepository The material type repository * @var MaterialTypeRepository The material type repository

@ -0,0 +1,101 @@
<?php
namespace App\Controller\Config;
use App\Entity\Recipe;
use App\Form\Config\RecipeEditForm;
use App\Misc\FlashType;
use App\Repository\RecipeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Controller for the configuration pages of material
*/
#[Route('/Config/Recipe')]
#[IsGranted('IS_AUTHENTICATED')]
class RecipeController extends AbstractController {
/**
* @var EntityManagerInterface The entity manager
*/
private readonly EntityManagerInterface $entityManager;
/**
* @var RecipeRepository The recipe repository
*/
private readonly RecipeRepository $recipeRepository;
/**
* Initialization
*
* @param EntityManagerInterface $entityManager The entity manager
* @param RecipeRepository $recipeRepository The material repository
*/
public function __construct (EntityManagerInterface $entityManager, RecipeRepository $recipeRepository) {
$this->entityManager = $entityManager;
$this->recipeRepository = $recipeRepository;
}
/**
* List of materials
*
* @return Response The response
*/
#[Route('/', name: 'config_recipe_list', alias: 'config_recipe')]
public function list (): Response {
return $this->render(
'Config/Recipe/List.html.twig',
[
'recipes' => $this->recipeRepository->findAll(),
]
);
}
/**
* Edit/Create a recipe
*
* @param Request $request The request
* @param Recipe|null $recipe The recipe to edit
*
* @return Response The response
*/
#[Route('/Create', name: 'config_recipe_create')]
#[Route('/Edit-{id}', name: 'config_recipe_edit')]
public function edit (Request $request, ?Recipe $recipe = null): Response {
$recipe ??= new Recipe();
$form = $this->createForm(RecipeEditForm::class, $recipe);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($recipe);
$this->entityManager->flush();
$this->addFlash(FlashType::SUCCESS, 'La recette a bien été enregistré.');
return $this->redirectToRoute('config_recipe_list');
}
return $this->render('Config/Recipe/Edit.html.twig', [
'recipe' => $recipe,
'form' => $form->createView(),
]);
}
/**
* Delete a recipe
*
* @param Recipe $recipe The recipe to delete
*
* @return Response The response
*/
#[Route('/Delete-{id}', name: 'config_recipe_delete')]
public function delete (Recipe $recipe): Response {
$this->entityManager->remove($recipe);
$this->entityManager->flush();
$this->addFlash(FlashType::SUCCESS, 'La recette a bien été supprimée.');
return $this->redirectToRoute('config_recipe_list');
}
}

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\InputRecipeMaterialRepository; use App\Repository\InputRecipeMaterialRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Stringable;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -11,14 +12,15 @@ use Symfony\Component\Validator\Constraints as Assert;
* A material consumed by a recipe * A material consumed by a recipe
*/ */
#[ORM\Entity(repositoryClass: InputRecipeMaterialRepository::class)] #[ORM\Entity(repositoryClass: InputRecipeMaterialRepository::class)]
#[ORM\UniqueConstraint(fields: ['recipe', 'material'])]
#[UniqueEntity(fields: ['recipe', 'material'], message: 'Ce matériau est déjà consommé par cette recette')] #[UniqueEntity(fields: ['recipe', 'material'], message: 'Ce matériau est déjà consommé par cette recette')]
class InputRecipeMaterial { class InputRecipeMaterial implements Stringable {
use TBaseEntity; use TBaseEntity;
/** /**
* @var Recipe|null The recipe * @var Recipe|null The recipe
*/ */
#[ORM\ManyToOne(inversedBy: 'inputMaterials')] #[ORM\ManyToOne(inversedBy: 'consumedMaterials')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull(message: 'Veuillez sélectionner une recette')] #[Assert\NotNull(message: 'Veuillez sélectionner une recette')]
#[Assert\Valid] #[Assert\Valid]
@ -41,6 +43,13 @@ class InputRecipeMaterial {
#[Assert\Positive(message: 'La quantité consommée doit être strictement positive')] #[Assert\Positive(message: 'La quantité consommée doit être strictement positive')]
private ?int $consumedQuantity = null; private ?int $consumedQuantity = null;
/**
* @inheritDoc
*/
public function __toString (): string {
return $this->getMaterial()->getName() . ' x' . $this->getConsumedQuantity();
}
/** /**
* The recipe * The recipe
* *

@ -33,8 +33,7 @@ class Material implements Stringable {
*/ */
#[ORM\Column] #[ORM\Column]
#[Assert\Type(type: 'bool', message: 'L\'indicateur de si le matériau est craftable par défaut doit être un booléen')] #[Assert\Type(type: 'bool', message: 'L\'indicateur de si le matériau est craftable par défaut doit être un booléen')]
#[Assert\NotNull(message: 'Veuillez indiquer si le matériau est craftable par défaut')] private ?bool $isCraftableByDefault = true;
private ?bool $isCraftableByDefault = null;
/** /**
* @var Collection<int, OutputRecipeMaterial> The recipes * @var Collection<int, OutputRecipeMaterial> The recipes

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\OutputRecipeMaterialRepository; use App\Repository\OutputRecipeMaterialRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Stringable;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -11,14 +12,15 @@ use Symfony\Component\Validator\Constraints as Assert;
* A material produced by a recipe * A material produced by a recipe
*/ */
#[ORM\Entity(repositoryClass: OutputRecipeMaterialRepository::class)] #[ORM\Entity(repositoryClass: OutputRecipeMaterialRepository::class)]
#[ORM\UniqueConstraint(fields: ['recipe', 'material'])]
#[UniqueEntity(fields: ['recipe', 'material'], message: 'Ce matériau est déjà produite par cette recette')] #[UniqueEntity(fields: ['recipe', 'material'], message: 'Ce matériau est déjà produite par cette recette')]
class OutputRecipeMaterial { class OutputRecipeMaterial implements Stringable {
use TBaseEntity; use TBaseEntity;
/** /**
* @var Recipe|null The recipe * @var Recipe|null The recipe
*/ */
#[ORM\ManyToOne(inversedBy: 'outputMaterials')] #[ORM\ManyToOne(inversedBy: 'producedMaterials')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull(message: 'Veuillez sélectionner une recette')] #[Assert\NotNull(message: 'Veuillez sélectionner une recette')]
#[Assert\Valid] #[Assert\Valid]
@ -41,6 +43,13 @@ class OutputRecipeMaterial {
#[Assert\Positive(message: 'La quantité produite doit être strictement positive')] #[Assert\Positive(message: 'La quantité produite doit être strictement positive')]
private ?float $producedQuantity = null; private ?float $producedQuantity = null;
/**
* @inheritDoc
*/
public function __toString (): string {
return $this->getMaterial()->getName() . ' x' . $this->getProducedQuantity();
}
/** /**
* The recipe * The recipe
* *

@ -13,9 +13,8 @@ use Symfony\Component\Validator\Constraints as Assert;
* A crafting recipe * A crafting recipe
*/ */
#[ORM\Entity(repositoryClass: RecipeRepository::class)] #[ORM\Entity(repositoryClass: RecipeRepository::class)]
class Recipe implements Stringable { class Recipe {
use TBaseEntity; use TBaseEntity;
use TNamedEntity;
/** /**
* @var Machine|null The crafting machine * @var Machine|null The crafting machine
@ -49,14 +48,14 @@ class Recipe implements Stringable {
/** /**
* @var Collection<int, InputRecipeMaterial> The consumed materials * @var Collection<int, InputRecipeMaterial> The consumed materials
*/ */
#[ORM\OneToMany(targetEntity: InputRecipeMaterial::class, mappedBy: 'recipe', orphanRemoval: true)] #[ORM\OneToMany(targetEntity: InputRecipeMaterial::class, mappedBy: 'recipe', cascade: ['persist'], orphanRemoval: true)]
#[Assert\Count(min: 1, minMessage: 'Veuillez ajouter au moins un matériau consommé')] #[Assert\Count(min: 1, minMessage: 'Veuillez ajouter au moins un matériau consommé')]
#[Assert\Valid] #[Assert\Valid]
private Collection $consumedMaterials; private Collection $consumedMaterials;
/** /**
* @var Collection<int, OutputRecipeMaterial> The produced materials * @var Collection<int, OutputRecipeMaterial> The produced materials
*/ */
#[ORM\OneToMany(targetEntity: OutputRecipeMaterial::class, mappedBy: 'recipe', orphanRemoval: true)] #[ORM\OneToMany(targetEntity: OutputRecipeMaterial::class, mappedBy: 'recipe', cascade: ['persist'], orphanRemoval: true)]
#[Assert\Count(min: 1, minMessage: 'Veuillez ajouter au moins un matériau produit')] #[Assert\Count(min: 1, minMessage: 'Veuillez ajouter au moins un matériau produit')]
#[Assert\Valid] #[Assert\Valid]
private Collection $producedMaterials; private Collection $producedMaterials;
@ -109,6 +108,7 @@ class Recipe implements Stringable {
$this->machineExtraInfo1 = $machineExtraInfo1; $this->machineExtraInfo1 = $machineExtraInfo1;
return $this; return $this;
} }
/** /**
* The machine's second extra information * The machine's second extra information
* *

@ -0,0 +1,44 @@
<?php
namespace App\Form\Config;
use App\Entity\InputRecipeMaterial;
use App\Repository\MaterialRepository;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* The form for an input material of a recipe
*/
class InputRecipeMaterialForm extends AbstractType {
/**
* @inheritDoc
*/
public function configureOptions (OptionsResolver $resolver): void {
$resolver->setDefaults(
[
'data_class' => InputRecipeMaterial::class,
]
);
}
/**
* @inheritDoc
*/
public function buildForm (FormBuilderInterface $builder, array $options): void {
$builder
->add('material', null, [
'label' => 'Matériau',
//'query_builder' => function (MaterialRepository $materialRepository): QueryBuilder {
// return $materialRepository->createQueryBuilder('m')
// ->orderBy('m.name');
//},
])
->add('consumedQuantity', null, [
'label' => 'Quantité consommée',
]);
}
}

@ -35,7 +35,8 @@ class MaterialEditForm extends AbstractType {
'label' => 'Type', 'label' => 'Type',
]) ])
->add('isCraftableByDefault', null, [ ->add('isCraftableByDefault', null, [
'label' => 'Est-ce que ce matériel est craftable par défaut ?', 'label' => 'Est-ce que ce matériel est craftable par défaut ?',
'required' => false,
]) ])
->add('submit', SubmitType::class, [ ->add('submit', SubmitType::class, [
'label' => 'Enregistrer', 'label' => 'Enregistrer',

@ -0,0 +1,37 @@
<?php
namespace App\Form\Config;
use App\Entity\OutputRecipeMaterial;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* The form for an output material of a recipe
*/
class OutputRecipeMaterialForm extends AbstractType {
/**
* @inheritDoc
*/
public function configureOptions (OptionsResolver $resolver): void {
$resolver->setDefaults(
[
'data_class' => OutputRecipeMaterial::class,
]
);
}
/**
* @inheritDoc
*/
public function buildForm (FormBuilderInterface $builder, array $options): void {
$builder
->add('material', null, [
'label' => 'Matériau',
])
->add('producedQuantity', null, [
'label' => 'Quantité produite',
]);
}
}

@ -0,0 +1,86 @@
<?php
namespace App\Form\Config;
use App\Entity\Machine;
use App\Entity\Recipe;
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\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* The form for editing a recipe
*/
class RecipeEditForm extends AbstractType {
/**
* @inheritDoc
*/
public function configureOptions (OptionsResolver $resolver): void {
$resolver->setDefaults(
[
'data_class' => Recipe::class,
]
);
}
/**
* @inheritDoc
*/
public function buildForm (FormBuilderInterface $builder, array $options): void {
$builder
->add('machine', null, [
'label' => 'Machine',
'choice_attr' => function (Machine $machine, string $index, string $machineId): array {
return [
'data-machine' => json_encode([
'labelExtraInfo1' => $machine->getLabelExtraInfo1(),
'labelExtraInfo2' => $machine->getLabelExtraInfo2(),
]),
];
},
])
->add('machineExtraInfo1', TextType::class, [
'label' => 'Information complémentaire n° 1',
'required' => false,
'row_attr' => [
'class' => 'd-none mb-3',
],
])
->add('machineExtraInfo2', null, [
'label' => 'Information complémentaire n° 2',
'required' => false,
'row_attr' => [
'class' => 'd-none mb-3',
],
])
->add('craftingTime', null, [
'label' => 'Temps de production',
])
->add('consumedMaterials', CollectionType::class, [
'label' => 'Matériaux consommés',
'entry_type' => InputRecipeMaterialForm::class,
'entry_options' => [
'label' => false,
],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
->add('producedMaterials', CollectionType::class, [
'label' => 'Matériaux produits',
'entry_type' => OutputRecipeMaterialForm::class,
'entry_options' => [
'label' => false,
],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
->add('submit', SubmitType::class, [
'label' => 'Enregistrer',
]);
}
}

@ -0,0 +1,20 @@
{% extends 'base.html.twig' %}
{% block title %}{% if recipe.id is null %}Création {% else %}Modification{% endif %} recette - {{ parent() }}{% endblock %}
{% block mainContent %}
<h1>
{% if recipe.id is null %}
Création nouvelle recette
{% else %}
Modification d'une recette
{% endif %}
</h1>
{{ form(form) }}
<a href="{{ path('config_recipe_list') }}" class="btn btn-danger">Annuler</a>
{% endblock %}
{% block importmap %}
{{ importmap(['app', 'config_recipe_edit']) }}
{% endblock %}

@ -0,0 +1,95 @@
{% extends '/base.html.twig' %}
{% block title %}Liste des recettes - {{ parent() }}{% endblock %}
{% block importmap %}{{ importmap(['app', 'datatables2']) }}{% endblock %}
{% block mainContent %}
<h1>Liste des recettes</h1>
<div class="d-flex">
<div class="table-responsive mnw-25">
<table class="table table-sm table-striped table-hover table-bordered table-datatable2 align-middle">
<thead>
<tr>
<th scope="col" data-sort-onLoad="1" class="align-middle">Matériaux produits</th>
<th scope="col" class="align-middle">Machine</th>
<th scope="col" class="align-middle">Matériaux consommés</th>
<th scope="col" data-sort="false" class="fit-content align-middle">
<a href="{{ path('config_recipe_create') }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-title="Ajouter">
<i class="fa-solid fa-square-plus"></i>
</a>
</th>
</tr>
</thead>
<tbody>
{% for recipe in recipes %}
{% set machine = recipe.machine %}
{% set firstExtraInfo = true %}
<tr>
<td>
{% for producedMaterial in recipe.producedMaterials %}
{% if not loop.first %}<br>{% endif %}{{ producedMaterial }}
{% endfor %}
</td>
<td
{% if machine.labelExtraInfo1 is not null or machine.labelExtraInfo2 is not null %}
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-title="
{% for extraInfo in 1..2 %}
{% if machine.('labelExtraInfo' ~ extraInfo) is not null %}
{% if firstExtraInfo %}
{% set firstExtraInfo = false %}
{% else %}
<br>
{% endif %}
{{ machine.('labelExtraInfo' ~ extraInfo) }} = {{ recipe.('machineExtraInfo' ~ extraInfo) }}
{% endif %}
{% endfor %}
"
{% endif %}
>
{{ machine }}
</td>
<td>
{% for consumedMaterial in recipe.consumedMaterials %}
{% if not loop.first %}<br>{% endif %}{{ consumedMaterial }}
{% endfor %}
</td>
<td class="fit-content">
<a href="{{ path('config_recipe_edit', {id: recipe.id}) }}"
class="text-primary me-2"
data-bs-toggle="tooltip"
data-bs-title="Éditer"
><i class="fa-solid fa-pen"></i></a>
<a href="#" class="text-danger" id="btDelete" data-bs-toggle="tooltip" data-bs-title="Supprimer">
<span data-bs-toggle="modal"
data-bs-target="#deleteConfirmation"
data-modal-dynamic-link-url="{{ path('config_recipe_delete', {id: recipe.id}) }}"
><i class="fa-solid fa-xmark"></i></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal modal-dynamic fade" id="deleteConfirmation" tabindex="-1" aria-label="btDelete" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Suppression</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Êtes-vous sûr de vouloir supprimer ce matériau ?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Non</button>
<a href="#" class="modal-confirm-link btn btn-danger">Oui</a>
</div>
</div>
</div>
</div>
{% endblock %}

@ -5,20 +5,7 @@
{% block mainContent %} {% block mainContent %}
<h1>Recipe Manager</h1> <h1>Recipe Manager</h1>
{% if app.user %} {% if app.user %}
<nav class="navbar navbar-expand-lg py-0"> <p>Bonjour {{ app.user }}</p>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle py-0" id="dropdown-config" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Configuration
</a>
<ul class="dropdown-menu" aria-labelledby="dropdown-config">
<li><a href="{{ path('config_materialType_list') }}" class="dropdown-item">Types de matériaux</a></li>
<li><a href="{{ path('config_machine_list') }}" class="dropdown-item">Machines</a></li>
<li><a href="{{ path('config_material_list') }}" class="dropdown-item">Matériaux</a></li>
</ul>
</li>
</ul>
</nav>
{% else %} {% else %}
<p>Bienvenu sur Recipe Manager, le gestionnaire de recette de jeux vidéos.</p> <p>Bienvenu sur Recipe Manager, le gestionnaire de recette de jeux vidéos.</p>
<p>Merci de vous <a href="{{ path('user_signIn') }}">connecter</a> ou <a href="{{ path('user_signOut') }}">créer un compte</a> pour commencer.</p> <p>Merci de vous <a href="{{ path('user_signIn') }}">connecter</a> ou <a href="{{ path('user_signOut') }}">créer un compte</a> pour commencer.</p>

@ -7,10 +7,29 @@
{% endblock %} {% endblock %}
{% block headerContent %} {% block headerContent %}
<div class="d-flex justify-content-between w-100 px-2"> <div class="d-flex justify-content-between w-100 px-2">
<!--region Website name--> <!--region Logo / Nom-->
<a class="navbar-brand" href="{{ path('core_main') }}">Recipe Manager</a> <a class="navbar-brand" href="{{ path('core_main') }}">Recipe Manager</a>
<!--endregion--> <!--endregion-->
<!--region menu--> <!--region Menu configuration-->
{% if app.user %}
<nav class="navbar navbar-expand-lg py-0">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle py-0" id="dropdown-config" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Configuration
</a>
<ul class="dropdown-menu" aria-labelledby="dropdown-config">
<li><a href="{{ path('config_recipe_list') }}" class="dropdown-item">Recettes</a></li>
<li><a href="{{ path('config_material_list') }}" class="dropdown-item">Matériaux</a></li>
<li><a href="{{ path('config_machine_list') }}" class="dropdown-item">Machines</a></li>
<li><a href="{{ path('config_materialType_list') }}" class="dropdown-item">Types de matériaux</a></li>
</ul>
</li>
</ul>
</nav>
{% endif %}
<!--endregion-->
<!--region Menu utilisateur-->
<nav class="navbar navbar-expand-lg py-0"> <nav class="navbar navbar-expand-lg py-0">
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if app.user %} {% if app.user %}

@ -0,0 +1,227 @@
{% use "bootstrap_5_horizontal_layout.html.twig" %}
{% block collection_widget %}
{% if prototype is defined and not prototype.rendered %}
{% set attr = attr|merge({
'data-prototype-code': form_row(prototype),
'data-prototype-placeholder': prototype.vars.name,
'data-prototype-index': prototype.parent.children|length > 0 ? prototype.parent.children|last.vars.name + 1 : 0,
}) %}
{% endif %}
{{ block('form_widget') }}
{% endblock collection_widget %}
{% block collection_entry_row %}
{% if expanded is defined and expanded %}
{{ block('fieldset_form_row') }}
{% else %}
{% set widget_attr = {context_collection_entry: true} %}
{% if help is not empty %}
{% set widget_attr = {attr: {'aria-describedby': id ~"_help"}} %}
{% endif %}
{% set row_class = row_class|default(row_attr.class|default('mb-3')) %}
{% set is_form_floating = is_form_floating|default('form-floating' in row_class) %}
{% set is_input_group = is_input_group|default('input-group' in row_class) %}
{#- Remove behavior class from the main container -#}
{% set row_class = row_class|replace({'form-floating': '', 'input-group': ''}) %}
<div{% with {attr: row_attr|merge({class: (row_class ~ ' row' ~ ((not compound or force_error|default(false)) and not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{% if is_form_floating or is_input_group %}
<div class="{{ block('form_label_class') }}"></div>
<div class="{{ block('form_group_class') }}">
{% if is_form_floating %}
<div class="form-floating">
{{ form_widget(form, widget_attr) }}
{{ form_label(form) }}
</div>
{% elseif is_input_group %}
<div class="input-group">
{{ form_label(form) }}
{{ form_widget(form, widget_attr) }}
{#- Hack to properly display help with input group -#}
{{ form_help(form) }}
</div>
{% endif %}
{% if not is_input_group %}
{{ form_help(form) }}
{% endif %}
{{ form_errors(form) }}
</div>
{% else %}
{{ form_label(form) }}
<div class="{% if label is same as false %}col-sm-12{% else %}col-sm-10{% endif %}">
{{ form_widget(form, widget_attr) }}
{{ form_help(form) }}
{{ form_errors(form) }}
</div>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block collection_entry_label %}
{% if label is same as false %}
{% else %}
{{ block('form_label') }}
{% endif %}
{% endblock %}
{% block collection_entry_widget %}
{% set attr = {'class': 'row row-cols-lg-auto g-3 align-items-center collection-row'} %}
{{ block('form_widget') }}
{% endblock %}
{% block collection_entry_add %}
<div class="mb-3 row">
<div class="col-12 column-gap-2">
<button type="button"
class="btn btn-primary collection-add"
data-bs-toggle="tooltip"
data-bs-title="Ajouter"
data-collection="{{ id }}"
>
<i class="fa-solid fa-square-plus"></i>
</button>
</div>
</div>
{% endblock %}
{% block collection_entry_delete %}
<div class="col-12 column-gap-2">
<button type="button"
class="btn btn-danger collection-row-delete"
data-bs-toggle="tooltip"
data-bs-title="Supprimer"
>
<i class="fa-solid fa-trash"></i>
</button>
</div>
{% endblock %}
{% block form_rows %}
{% set child_vars = {} %}
{% if not context_collection_entry|default(false) %}
{% if allow_add is defined %}
{% set child_vars = child_vars|merge({'allow_add': allow_add}) %}
{% endif %}
{% if allow_delete is defined %}
{% set child_vars = child_vars|merge({'allow_delete': allow_delete}) %}
{% endif %}
{% endif %}
{% if context_collection_entry is defined %}
{% set child_vars = child_vars|merge({'context_collection_entry': context_collection_entry}) %}
{% endif %}
{% for child in form|filter(child => not child.rendered) %}
{{ form_row(child, child_vars) }}
{% endfor %}
{% if context_collection_entry|default(false) %}
{% if allow_delete|default(false) %}
{{ block('collection_entry_delete') }}
{% endif %}
{% else %}
{% if allow_add|default(false) %}
{{ block('collection_entry_add') }}
<div id="{{ id }}__collection_entry_add-delete_button" class="d-none">
{{ block('collection_entry_delete') }}
</div>
{% endif %}
{% endif %}
{% endblock form_rows %}
{% block form_row %}
{% set context_collection_entry = context_collection_entry|default(false) %}
{% if expanded is defined and expanded %}
{{ block('fieldset_form_row') }}
{% else %}
{% set widget_attr = {} %}
{% if help is not empty %}
{% set widget_attr = {attr: {'aria-describedby': id ~"_help"}} %}
{% endif %}
{% set row_class = row_class|default(row_attr.class|default(context_collection_entry ? '' : 'mb-3')) %}
{% set is_form_floating = is_form_floating|default('form-floating' in row_class) %}
{% set is_input_group = is_input_group|default('input-group' in row_class) %}
{#- Remove behavior class from the main container -#}
{% set row_class = row_class|replace({'form-floating': '', 'input-group': ''}) %}
<div
{% with {
attr: row_attr|merge({
class: (
row_class
~ (not context_collection_entry ? ' row' : ' col-12 d-flex column-gap-2')
~ ((not compound or force_error|default(false)) and not valid ? ' is-invalid')
)|trim
})
} %}
{{ block('attributes') }}
{% endwith %}
>
{% if is_form_floating or is_input_group %}
<div class="{{ block('form_label_class') }}"></div>
<div class="{{ block('form_group_class') }}">
{% if is_form_floating %}
<div class="form-floating">
{{ form_widget(form, widget_attr) }}
{{ form_label(form) }}
</div>
{% elseif is_input_group %}
<div class="input-group">
{{ form_label(form) }}
{{ form_widget(form, widget_attr) }}
{#- Hack to properly display help with input group -#}
{{ form_help(form) }}
</div>
{% endif %}
{% if not is_input_group %}
{{ form_help(form) }}
{% endif %}
{{ form_errors(form) }}
</div>
{% else %}
{{ form_label(form) }}
<div class="{{ not context_collection_entry ? block('form_group_class') }}">
{{ form_widget(form, widget_attr) }}
{{ form_help(form) }}
{{ form_errors(form) }}
</div>
{% endif %}
</div>
{% endif %}
{% endblock form_row %}
{% block form_label -%}
{% set context_collection_entry = context_collection_entry|default(false) %}
{%- if label is same as(false) -%}
<div class="{{ block('form_label_class') }}"></div>
{%- else -%}
{%- set row_class = row_class|default(row_attr.class|default('')) -%}
{%- if 'form-floating' not in row_class and 'input-group' not in row_class -%}
{%- if expanded is not defined or not expanded -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label')|trim}) -%}
{%- endif -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ (not context_collection_entry ? block('form_label_class')))|trim}) -%}
{%- endif -%}
{% if label is not same as(false) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- if compound is defined and compound -%}
{%- set element = 'legend' -%}
{%- if 'col-form-label' not in parent_label_class -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label' )|trim}) -%}
{%- endif -%}
{%- else -%}
{%- set row_class = row_class|default(row_attr.class|default('')) -%}
{%- set label_attr = label_attr|merge({for: id}) -%}
{%- if 'col-form-label' not in parent_label_class -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ('input-group' in row_class ? ' input-group-text' : ' form-label') )|trim}) -%}
{%- endif -%}
{%- endif -%}
{%- endif -%}
{% if label is not same as(false) -%}
{% if not compound -%}
{% set label_attr = label_attr|merge({'for': id}) %}
{%- endif -%}
{% if required -%}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
{%- endif -%}
<{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>
{{- block('form_label_content') -}}
</{{ element|default('label') }}>
{%- endif -%}
{%- endif -%}
{%- endblock form_label %}
Loading…
Cancel
Save