feat(artemis/esyweb): Ajoute la gestion des tutoriels ESY-WEB

Crée une page pour lister et gérer les tutoriels ESY-WEB dans Artemis.
Ajoute un formulaire pour créer de nouveaux tutoriels.
Gère l'upload de fichiers mp4 pour les tutoriels.
This commit is contained in:
Serreau Jovann
2025-09-30 13:26:57 +02:00
parent aa0e2a281d
commit ee8ba6b2df
15 changed files with 563 additions and 5 deletions

View File

@@ -16,10 +16,37 @@ export class ConfirmModal extends HTMLAnchorElement{
if(element.getAttribute('type') == "delete-email") {
this.deleteEmail(modalConfirm,element.getAttribute('href'));
}
if(element.getAttribute('type') == "delete-esyweb-tuto") {
this.deleteEsyWebTuto(modalConfirm,element.getAttribute('href'));
}
document.body.appendChild(modalConfirm);
})
}
deleteEsyWebTuto(modalConfirm,link) {
let message = document.createElement('h2');
message.innerText = "Confirmer la suppression du tutoriel";
modalConfirm.querySelector('.confirm-modal-content').appendChild(message);
let grid = document.createElement('div')
grid.classList = "grid grid-cols-1 gap-4 md:grid-cols-2";
modalConfirm.querySelector('.confirm-modal-content').appendChild(grid);
let buttonOk = document.createElement('button');
buttonOk.classList = "bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded";
buttonOk.innerText = "Oui";
buttonOk.addEventListener('click',()=>{
modalConfirm.remove()
location.href = link;
})
let buttonKo = document.createElement('button');
buttonKo.classList = "bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded";
buttonKo.innerText = "Non";
buttonKo.addEventListener('click',()=>modalConfirm.remove())
grid.appendChild(buttonOk)
grid.appendChild(buttonKo)
}
deleteEmail(modalConfirm,link) {
let message = document.createElement('h2');
message.innerText = "Confirmer la suppression de l'email";

View File

@@ -57,6 +57,13 @@ vich_uploader:
inject_on_load: false
delete_on_update: true
delete_on_remove: true
tuto:
uri_prefix: /storage/tuto
upload_destination: '%kernel.project_dir%/public/storage/tuto'
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer # Replaced namer
inject_on_load: false
delete_on_update: true
delete_on_remove: true
#mappings:
# products:
# uri_prefix: /images/products

View File

@@ -67,6 +67,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends git \
&& apt-get autoremove -y build-essential git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY ./docker/php/custom.ini /usr/local/etc/php/conf.d/custom.ini
RUN echo "zend_extension=xdebug" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN echo "xdebug.mode=develop,debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

2
docker/php/custom.ini Normal file
View File

@@ -0,0 +1,2 @@
upload_max_filesize=128M
post_max_size=128M

View File

@@ -0,0 +1,33 @@
<?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 Version20250930100019 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('CREATE TABLE esy_web_tuto (id SERIAL NOT NULL, title TEXT NOT NULL, file_name VARCHAR(255) DEFAULT NULL, file_dimensions JSON DEFAULT NULL, file_size VARCHAR(255) DEFAULT NULL, file_mine_type VARCHAR(255) DEFAULT NULL, file_original_name VARCHAR(255) DEFAULT NULL, update_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN esy_web_tuto.update_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP TABLE esy_web_tuto');
}
}

View File

@@ -0,0 +1,32 @@
<?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 Version20250930104036 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('ALTER TABLE esy_web_tuto ADD pos INT NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE esy_web_tuto DROP pos');
}
}

View File

@@ -3,18 +3,23 @@
namespace App\Controller\ApiInterne;
use App\Entity\Compute;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerAdvertPaymentLine;
use App\Entity\CustomerAdvertPaymentRegister;
use App\Repository\CustomerAdvertPaymentRepository;
use App\Service\Customer\CreateAvisEvent;
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\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class RegisterController extends AbstractController
{
#[Route(path: '/api-interne/intranet/customer/register',name: 'api-interne-intranet-customer-register')]
public function customerRegisterPayment(Request $request,CustomerAdvertPaymentRepository $customerAdvertPaymentRepository): Response {
public function customerRegisterPayment(EventDispatcherInterface $eventDispatcher,EntityManagerInterface $entityManager,Request $request,CustomerAdvertPaymentRepository $customerAdvertPaymentRepository): Response {
$content = $request->getContent();
$content = json_decode($content);
$format = [];
@@ -33,15 +38,39 @@ class RegisterController extends AbstractController
$advertRegister->setAmount(floatval($format['amount']));
$advertRegister->setNumeroRemise($format['num_cheque']);
$advertRegister->setChequeNum($format['num_cheque']);
$entityManager->persist($advertRegister);
//if complete paiement generated fac
if(floatval($format['num_cheque']) < $total) {
//send mail with no completed
$diff = $total - floatval($format['amount']);
$diff = $total - floatval($format['amount']);
if($diff >0){
$t = new \DateTimeImmutable();
$num = 'A-' . $t->format('Y/m') . '/' . sprintf('%05d', $entityManager->getRepository(CustomerAdvertPayment::class)->count() + 1);
$diffAdvert = new CustomerAdvertPayment();
$diffAdvert->setCustomer($advertRegister->getAdvert()->getCustomer());
$diffAdvert->setDevis($advertRegister->getAdvert()->getDevis());
$diffAdvert->setUpdateAt(\DateTimeImmutable::createFromFormat('Y-m-d', $format['date']));
$diffAdvert->setCreateAt(\DateTimeImmutable::createFromFormat('Y-m-d', $format['date']));
$diffAdvert->setNumAvis($num);
$diffAdvert->setState('created');
$entityManager->persist($diffAdvert);
$diffAdvertLine = new CustomerAdvertPaymentLine();
$diffAdvertLine->setPos(0);
$diffAdvertLine->setTva(1.20);
$diffAdvertLine->setName("Différence de paiement ".$advertRegister->getAdvert()->getNumAvis());
$diffAdvertLine->setPriceHt($diff / 1.20);
$diffAdvertLine->setContent("");
$entityManager->persist($diffAdvertLine);
$diffAdvert->addCustomerAdvertPaymentLine($diffAdvertLine);
$entityManager->persist($diffAdvert);
$entityManager->flush();
$event = new CreateAvisEvent($diffAdvert, true);
$eventDispatcher->dispatch($event);
return $this->json([]);
} else {
dd("completed");
}
//if no completed
// generated confirmation payement
// generate new advert payment

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Controller\Artemis\EsyWeb;
use App\Entity\EsyWebTuto;
use App\Form\Artemis\EsyWeb\TutoType;
use App\Repository\EsyWebTutoRepository;
use App\Service\Logger\LoggerService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
class TutoController extends AbstractController
{
#[Route(path: '/artemis/esyweb/tuto/add', name: 'artemis_esyweb_tuto_add', methods: ['GET', 'POST'])]
public function tutosAdd(Request $request,LoggerService $loggerService,EsyWebTutoRepository $esyWebTutoRepository,EntityManagerInterface $entityManager)
{
$tuto = new EsyWebTuto();
$tuto->setPos($esyWebTutoRepository->count());
$tuto->setUpdateAt(new \DateTimeImmutable());
$form = $this->createForm(TutoType::class,$tuto);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($tuto);
$entityManager->flush();
$loggerService->log("SUCCESS","Crée un tutoriel");
$this->addFlash("success","Tutoriel crée un tutoriel");
return $this->redirectToRoute('artemis_esyweb_tuto');
}
return $this->render('artemis/esyweb/tuto_add.twig',[
'form' => $form->createView(),
]);
}
#[Route(path: '/artemis/esyweb/tuto', name: 'artemis_esyweb_tuto', methods: ['GET', 'POST'])]
public function tutos(KernelInterface $kernel,Request $request,UploaderHelper $uploaderHelper,LoggerService $loggerService,EsyWebTutoRepository $esyWebTutoRepository,EntityManagerInterface $entityManager)
{
if($request->query->has('idDelete')) {
$ndd = $esyWebTutoRepository->find($request->query->get('idDelete'));
$entityManager->remove($ndd);
$entityManager->flush();
$loggerService->log("DELETE","Suppression d'un tutoriel");
$this->addFlash("success","Tutoriel bien supprimer");
return $this->redirectToRoute('artemis_esyweb_tuto');
}
if($request->query->has('idView')) {
$ndd = $esyWebTutoRepository->find($request->query->get('idView'));
$path = $uploaderHelper->asset($ndd,'file');
$response = new BinaryFileResponse($kernel->getProjectDir()."/public".$path);
return $response;
}
return $this->render('artemis/esyweb/tuto.twig',[
'tutos' => $esyWebTutoRepository->findAll(),
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Controller;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
class TutoController extends AbstractController
{
#[Route(path: '/tuto',name: 'app_tuto',methods: ['GET', 'POST'])]
public function tuto(): Response
{
}
}

183
src/Entity/EsyWebTuto.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
namespace App\Entity;
use App\Repository\EsyWebTutoRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
#[ORM\Entity(repositoryClass: EsyWebTutoRepository::class)]
#[Vich\Uploadable()]
class EsyWebTuto
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $title = null;
#[Vich\UploadableField(mapping: 'tuto',fileNameProperty: 'fileName', size: 'fileSize', mimeType: 'fileMineType', originalName: 'fileOriginalName',dimensions: 'fileDimensions')]
private ?File $file = null;
#[ORM\Column(nullable: true)]
private ?string $fileName = null;
#[ORM\Column(nullable: true)]
private ?array $fileDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $fileSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $fileMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $fileOriginalName = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updateAt;
#[ORM\Column]
private ?int $pos = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
/**
* @return string|null
*/
public function getFileSize(): ?string
{
return $this->fileSize;
}
/**
* @return string|null
*/
public function getFileOriginalName(): ?string
{
return $this->fileOriginalName;
}
/**
* @return \DateTimeImmutable|null
*/
public function getUpdateAt(): ?\DateTimeImmutable
{
return $this->updateAt;
}
/**
* @return string|null
*/
public function getFileName(): ?string
{
return $this->fileName;
}
/**
* @return string|null
*/
public function getFileMineType(): ?string
{
return $this->fileMineType;
}
/**
* @return array|null
*/
public function getFileDimensions(): ?array
{
return $this->fileDimensions;
}
/**
* @return File|null
*/
public function getFile(): ?File
{
return $this->file;
}
/**
* @param File|null $file
*/
public function setFile(?File $file): void
{
$this->file = $file;
}
/**
* @param \DateTimeImmutable|null $updateAt
*/
public function setUpdateAt(?\DateTimeImmutable $updateAt): void
{
$this->updateAt = $updateAt;
}
/**
* @param array|null $fileDimensions
*/
public function setFileDimensions(?array $fileDimensions): void
{
$this->fileDimensions = $fileDimensions;
}
/**
* @param string|null $fileMineType
*/
public function setFileMineType(?string $fileMineType): void
{
$this->fileMineType = $fileMineType;
}
/**
* @param string|null $fileName
*/
public function setFileName(?string $fileName): void
{
$this->fileName = $fileName;
}
/**
* @param string|null $fileSize
*/
public function setFileSize(?string $fileSize): void
{
$this->fileSize = $fileSize;
}
/**
* @param string|null $fileOriginalName
*/
public function setFileOriginalName(?string $fileOriginalName): void
{
$this->fileOriginalName = $fileOriginalName;
}
public function getPos(): ?int
{
return $this->pos;
}
public function setPos(int $pos): static
{
$this->pos = $pos;
return $this;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Form\Artemis\EsyWeb;
use App\Entity\EsyWebTuto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TutoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title',TextType::class,[
'label' => 'Titre du tutoriels',
'required' => true,
])
->add('file',FileType::class,[
'label' => 'Fichier du tutoriel (mp4)',
'attr' => [
'accept' => 'video/mp4'
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class',EsyWebTuto::class);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\EsyWebTuto;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EsyWebTuto>
*/
class EsyWebTutoRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EsyWebTuto::class);
}
// /**
// * @return EsyWebTuto[] Returns an array of EsyWebTuto objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('e.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?EsyWebTuto
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -65,6 +65,22 @@
<span class="ml-3">Tableau de bord</span>
</a>
</li>
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="cmsesyweb">
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path><path d="M12 2.252A8.014 8.014 0 0117.748 12H12V2.252z"></path></svg>
<span class="ml-3">CMS ESY-WEB</span>
</div>
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 arrow-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
</button>
<ul id="submenu-cmsesyweb" class="submenu ml-6 mt-2 space-y-2">
<li>
<a href="{{ path('artemis_esyweb_tuto') }}" class="flex items-center p-2 text-sm font-normal text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<span class="ml-3">Tutoriels</span>
</a>
</li>
</ul>
</li>
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="infrastructure">
<div class="flex items-center">

View File

@@ -0,0 +1,41 @@
{% extends 'artemis/base.twig' %}
{% block title %}Tutoriel(s){% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">Liste des tutoriels</h2>
<div>
<a href="{{ path('artemis_esyweb_tuto_add') }}" class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
+ Crée un tutoriel
</a>
<a target="_blank" href="{{ path('app_tuto') }}" class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
Voir la page des tutoriels
</a>
</div>
</div>
<div class="mt-2 w-full mx-auto bg-gray-800 rounded-lg shadow-lg overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Titre</th>
<th scope="col" class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
{% for tuto in tutos %}
<tr class="hover:bg-gray-700 transition relative hover:bg-gray-700 transition">
<td class="px-6 py-4 text-center text-sm w-80">{{ tuto.title }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-center w-20">
<a href="{{ path('artemis_esyweb_tuto',{idView:tuto.id}) }}" target="_blank"
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded mr-2">Voir</a>
<a data-turbo="false" is="confirm-modal" type="delete-esyweb-tuto"
href="{{ path('artemis_esyweb_tuto',{idDelete:tuto.id}) }}"
class="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded">Supprimer</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'artemis/base.twig' %}
{% block title %}Crée un tutoriel(s){% endblock %}
{% block content %}
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">Crée un tutoriels</h2>
<div class="mt-5 bg-gray-800 rounded-lg shadow-lg p-6 space-y-4">
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.file) }}
<button type="submit" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold px-4 py-2 rounded">Enregistrer</button>
{{ form_end(form) }}
</div>
{% endblock %}