✨ feat(artemis/tools): Ajoute le partage de fichiers temporaires avec QR code
Ajoute une fonctionnalité de partage de fichiers temporaires avec suppression automatique après 30 minutes et génération de QR codes. Ajoute aussi une tâche cron pour supprimer ces fichiers.
This commit is contained in:
@@ -241,6 +241,14 @@
|
||||
user: "root"
|
||||
job: "php {{ path }}/bin/console mainframe:cron:sync"
|
||||
state: present
|
||||
- name: "Cron Task sync"
|
||||
ansible.builtin.cron:
|
||||
name: "Mainframe - Delete tmp"
|
||||
minute: "30"
|
||||
hour: "*"
|
||||
user: "root"
|
||||
job: "php {{ path }}/bin/console mainframe:tempfile:delete"
|
||||
state: present
|
||||
- name: "Mail event today"
|
||||
ansible.builtin.cron:
|
||||
name: "Mainframe - Event Today"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
commande.siteconseil.fr, tutoriel.esy-web.dev, mainframe.esy-web.dev {
|
||||
commande.siteconseil.fr, tutoriel.esy-web.dev, mainframe.esy-web.dev, partage.siteconseil.fr {
|
||||
tls {
|
||||
dns cloudflare bnbe6SmF2kYBnDi4rEeoPI0wNXeFDWn0xZv7Dnfp
|
||||
}
|
||||
|
||||
@@ -472,3 +472,15 @@ confirm-modal{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative !important;
|
||||
max-width: 50vw;
|
||||
h3 {
|
||||
text-align: center;
|
||||
display: block;
|
||||
strong {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,13 @@ vich_uploader:
|
||||
inject_on_load: false
|
||||
delete_on_update: true
|
||||
delete_on_remove: true
|
||||
tools_share:
|
||||
uri_prefix: /storage/tools_share
|
||||
upload_destination: '%kernel.project_dir%/public/storage/tools_share'
|
||||
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
|
||||
|
||||
34
migrations/Version20251105090734.php
Normal file
34
migrations/Version20251105090734.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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 Version20251105090734 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 temp_file (id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, create_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, update_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, share_file_name VARCHAR(255) DEFAULT NULL, share_dimensions JSON DEFAULT NULL, share_size VARCHAR(255) DEFAULT NULL, share_mine_type VARCHAR(255) DEFAULT NULL, share_original_name VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('COMMENT ON COLUMN temp_file.create_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN temp_file.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 temp_file');
|
||||
}
|
||||
}
|
||||
64
src/Command/TempFileDeleteCommand.php
Normal file
64
src/Command/TempFileDeleteCommand.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Repository\TempFileRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
// Note: KernelInterface a été retiré car nous ne gérons plus la suppression physique de fichiers.
|
||||
|
||||
#[AsCommand(name: 'mainframe:tempfile:delete', description: 'Deletes expired temporary file records from the database only.')]
|
||||
class TempFileDeleteCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Define the expiration interval. Records older than this will be deleted.
|
||||
*/
|
||||
private const EXPIRATION_INTERVAL = '30 minutes'; // Intervalle ajusté à 30 minutes
|
||||
|
||||
public function __construct(
|
||||
private readonly TempFileRepository $tempFileRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
// Suppression de KernelInterface car la suppression physique est désactivée.
|
||||
?string $name = null
|
||||
) {
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Temporary File Database Cleanup');
|
||||
|
||||
$now = new \DateTimeImmutable();
|
||||
$deletedCount = 0;
|
||||
|
||||
$io->info(sprintf('Checking for database records older than %s (based on creation time)...', self::EXPIRATION_INTERVAL));
|
||||
|
||||
foreach ($this->tempFileRepository->findAll() as $file) {
|
||||
/** @var \App\Entity\TempFile $file */
|
||||
// CORRECTION: Utilisation de getCreateAt() pour la date de base d'expiration
|
||||
$createdAt = $file->getCreateAt();
|
||||
|
||||
// Calculate the expiration threshold: createdAt + EXPIRATION_INTERVAL
|
||||
$expiryThreshold = $createdAt->modify('+' . self::EXPIRATION_INTERVAL);
|
||||
// Check if the record has expired
|
||||
if ($expiryThreshold < $now) {
|
||||
// 1. Delete the database entity only (via Doctrine)
|
||||
$this->entityManager->remove($file);
|
||||
$deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Persist all database changes at once
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success(sprintf('Cleanup complete. %d temporary file records were deleted from the database.', $deletedCount));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
107
src/Controller/Artemis/Tools/ShareController.php
Normal file
107
src/Controller/Artemis/Tools/ShareController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Artemis\Tools;
|
||||
|
||||
use App\Entity\TempFile;
|
||||
use App\Repository\TempFileRepository;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel;
|
||||
use Endroid\QrCode\RoundBlockSizeMode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Vich\UploaderBundle\Handler\DownloadHandler;
|
||||
use Vich\UploaderBundle\Handler\UploadHandler;
|
||||
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
|
||||
use Vich\UploaderBundle\Templating\Helper\UploaderHelperInterface;
|
||||
|
||||
class ShareController extends AbstractController
|
||||
{
|
||||
#[Route('/artemis/tools/share', name: 'artemis_tools_share', methods: ['GET', 'POST'])]
|
||||
public function toolsShare(TempFileRepository $tempFileRepository,Request $request,EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
if($_ENV['APP_ENV'] !== 'DEV') {
|
||||
$baseUrl = "https://partage.siteconseil.fr";
|
||||
} else {
|
||||
$baseUrl = "http://esyweb.local";
|
||||
}
|
||||
if($request->isMethod('POST')) {
|
||||
/** @var UploadedFile $file */
|
||||
$file = $request->files->get('file_upload');
|
||||
$name= $request->request->all()['file_name'];
|
||||
if($name == "") {
|
||||
$name = $file->getClientOriginalName();
|
||||
} else {
|
||||
$name = $name.".".$file->getClientOriginalExtension();
|
||||
}
|
||||
$t = new \DateTimeImmutable();
|
||||
$t = $t->modify("+30 minutes");
|
||||
$temp = new TempFile();
|
||||
$temp->setName($name);
|
||||
$temp->setCreateAt(new \DateTimeImmutable());
|
||||
$temp->setUpdateAt($t);
|
||||
$temp->setShare($file);
|
||||
$entityManager->persist($temp);
|
||||
$entityManager->flush();
|
||||
$this->addFlash("success","Création effectuée");
|
||||
return $this->redirectToRoute('artemis_tools_share');
|
||||
}
|
||||
|
||||
$t = new \DateTimeImmutable();
|
||||
$files = [];
|
||||
foreach ( $tempFileRepository->findBy([],['id'=>'ASC']) as $item) {
|
||||
|
||||
$item->expires_in = $t->diff($item->getUpdateAt())->i; // Use 'i' for minutes
|
||||
$files[] = $item;
|
||||
}
|
||||
|
||||
return $this->render('artemis/tools/share.twig', [
|
||||
'baseUrl' => $baseUrl,
|
||||
'files' => $files,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/artemis/tools/share/view/{id}', name: 'artemis_tools_share_view', methods: ['GET', 'POST'])]
|
||||
public function toolsShareView(?TempFile $tempFile,DownloadHandler $downloadHandler): Response
|
||||
{
|
||||
return $downloadHandler->downloadObject($tempFile,"share");
|
||||
}
|
||||
#[Route('/artemis/tools/share/delete/{id}', name: 'artemis_tools_share_delete', methods: ['GET', 'POST'])]
|
||||
public function toolsShareDelete(?TempFile $tempFile,EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
$entityManager->remove($tempFile);
|
||||
$entityManager->flush();
|
||||
return $this->redirectToRoute('artemis_tools_share');
|
||||
}
|
||||
#[Route('/artemis/tools/share/qrCode/{id}', name: 'artemis_tools_share_qrcode', methods: ['GET', 'POST'])]
|
||||
public function toolsShareQrcode(?TempFile $tempFile,UploaderHelper $uploaderHelper): Response
|
||||
{
|
||||
if($_ENV['APP_ENV'] != 'DEV') {
|
||||
$baseUrl = "https://partage.siteconseil.fr";
|
||||
} else {
|
||||
$baseUrl = "http://esyweb.local";
|
||||
}
|
||||
$builder = new Builder(
|
||||
writer: new PngWriter(),
|
||||
writerOptions: [],
|
||||
validateResult: false,
|
||||
data: $baseUrl.$uploaderHelper->asset($tempFile,"share"),
|
||||
encoding: new Encoding('UTF-8'),
|
||||
errorCorrectionLevel: ErrorCorrectionLevel::High,
|
||||
size: 150,
|
||||
margin: 1,
|
||||
roundBlockSizeMode: RoundBlockSizeMode::Margin,
|
||||
);
|
||||
|
||||
$result = $builder->build();
|
||||
|
||||
return new Response($result->getString(),200,['Content-Type'=>$result->getMimeType()]);
|
||||
}
|
||||
}
|
||||
@@ -738,6 +738,7 @@ class HomeController extends AbstractController
|
||||
|
||||
return $this->render('order/flow.twig', [
|
||||
'form' => $form->createView(),
|
||||
'currentFormule' => self::formulesList[$order->getIdFormule()],
|
||||
'formules' => self::formules,
|
||||
]);
|
||||
}
|
||||
|
||||
178
src/Entity/TempFile.php
Normal file
178
src/Entity/TempFile.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\TempFileRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Vich\UploaderBundle\Mapping\Annotation as Vich;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TempFileRepository::class)]
|
||||
#[Vich\Uploadable()]
|
||||
class TempFile
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createAt = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $updateAt = null;
|
||||
|
||||
#[Vich\UploadableField(mapping: 'tools_share',fileNameProperty: 'shareFileName', size: 'shareSize', mimeType: 'shareMineType', originalName: 'shareOriginalName',dimensions: 'shareDimensions')]
|
||||
private ?File $share = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?string $shareFileName = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?array $shareDimensions = [];
|
||||
#[ORM\Column(length: 255,nullable: true)]
|
||||
private ?string $shareSize = null;
|
||||
#[ORM\Column(length: 255,nullable: true)]
|
||||
private ?string $shareMineType = null;
|
||||
#[ORM\Column(length: 255,nullable: true)]
|
||||
private ?string $shareOriginalName = null;
|
||||
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreateAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createAt;
|
||||
}
|
||||
|
||||
public function setCreateAt(\DateTimeImmutable $createAt): static
|
||||
{
|
||||
$this->createAt = $createAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdateAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updateAt;
|
||||
}
|
||||
|
||||
public function setUpdateAt(\DateTimeImmutable $updateAt): static
|
||||
{
|
||||
$this->updateAt = $updateAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return File|null
|
||||
*/
|
||||
public function getShare(): ?File
|
||||
{
|
||||
return $this->share;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File|null $share
|
||||
*/
|
||||
public function setShare(?File $share): void
|
||||
{
|
||||
$this->share = $share;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getShareDimensions(): ?array
|
||||
{
|
||||
return $this->shareDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getShareFileName(): ?string
|
||||
{
|
||||
return $this->shareFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getShareMineType(): ?string
|
||||
{
|
||||
return $this->shareMineType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getShareOriginalName(): ?string
|
||||
{
|
||||
return $this->shareOriginalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getShareSize(): ?string
|
||||
{
|
||||
return $this->shareSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $shareDimensions
|
||||
*/
|
||||
public function setShareDimensions(?array $shareDimensions): void
|
||||
{
|
||||
$this->shareDimensions = $shareDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $shareFileName
|
||||
*/
|
||||
public function setShareFileName(?string $shareFileName): void
|
||||
{
|
||||
$this->shareFileName = $shareFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $shareMineType
|
||||
*/
|
||||
public function setShareMineType(?string $shareMineType): void
|
||||
{
|
||||
$this->shareMineType = $shareMineType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $shareOriginalName
|
||||
*/
|
||||
public function setShareOriginalName(?string $shareOriginalName): void
|
||||
{
|
||||
$this->shareOriginalName = $shareOriginalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $shareSize
|
||||
*/
|
||||
public function setShareSize(?string $shareSize): void
|
||||
{
|
||||
$this->shareSize = $shareSize;
|
||||
}
|
||||
}
|
||||
43
src/Repository/TempFileRepository.php
Normal file
43
src/Repository/TempFileRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\TempFile;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TempFile>
|
||||
*/
|
||||
class TempFileRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TempFile::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return TempFile[] Returns an array of TempFile objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('t.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?TempFile
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -150,6 +150,22 @@
|
||||
</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="tools">
|
||||
<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">Outils</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-tools" class="submenu ml-6 mt-2 space-y-2">
|
||||
<li>
|
||||
<a href="{{ path('artemis_tools_share') }}" 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">Partage de fichier temporaire</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<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="settings">
|
||||
|
||||
169
templates/artemis/tools/share.twig
Normal file
169
templates/artemis/tools/share.twig
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends 'artemis/base.twig' %}
|
||||
{% block title %}Partage de fichier temporaire{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
{#
|
||||
// =================================================================
|
||||
// 🚀 SECTION AJOUTER UN NOUVEAU FICHIER
|
||||
// =================================================================
|
||||
#}
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-gray-100 mb-6 flex items-center">
|
||||
<span class="mr-3">📤</span> Ajouter un nouveau fichier temporaire
|
||||
</h2>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
{# Le formulaire utilise la route 'artemis_tools_share_upload' (à définir dans votre application) #}
|
||||
<form action="{{ path('artemis_tools_share') }}" method="POST" enctype="multipart/form-data">
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{# Champ de sélection de fichier #}
|
||||
<div>
|
||||
<label for="file_upload" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Fichier à partager
|
||||
</label>
|
||||
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 dark:border-gray-600 border-dashed rounded-md hover:border-indigo-500 dark:hover:border-indigo-400">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m2 0l-4 4m0 0l-4-4m-2 2l-2-2m-2 2l-2-2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-gray-600 dark:text-gray-400">
|
||||
<label for="file_upload" class="relative cursor-pointer bg-white dark:bg-gray-800 rounded-md font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500 dark:focus-within:ring-offset-gray-800">
|
||||
<span>Sélectionner un fichier</span>
|
||||
<input id="file_upload" name="file_upload" type="file" required class="sr-only">
|
||||
</label>
|
||||
<p class="pl-1">ou glisser-déposer ici</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Max 50MB. Tous types de fichiers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Champ pour le nom (optionnel, le nom du fichier sera par défaut) #}
|
||||
<div>
|
||||
<label for="file_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Nom du partage (Optionnel)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="text" name="file_name" id="file_name" placeholder="Laisser vide pour utiliser le nom original" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" maxlength="100">
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Le fichier sera automatiquement supprimé après un court délai.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Bouton de soumission #}
|
||||
<div class="mt-6">
|
||||
<button type="submit" class="w-full justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 flex items-center">
|
||||
<i class="bi bi-upload mr-2"></i> Téléverser et partager temporairement
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{#
|
||||
// =================================================================
|
||||
// 📋 SECTION LISTE DES FICHIERS EXISTANTS
|
||||
// =================================================================
|
||||
#}
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-gray-100 mb-6 flex items-center">
|
||||
<span class="mr-3">📋</span> Liste des fichiers temporaires
|
||||
</h2>
|
||||
|
||||
{% if files is empty %}
|
||||
{# Alerte #}
|
||||
<div class="bg-blue-100 dark:bg-blue-500 border-l-4 border-blue-500 text-blue-700 dark:text-blue-200 p-4" role="alert">
|
||||
<p class="font-bold">Information</p>
|
||||
<p><strong>Aucun fichier temporaire</strong> n'est actuellement disponible.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Tableau... (Reste du code du tableau) #}
|
||||
<div class="shadow overflow-hidden border-b border-gray-200 dark:border-gray-700 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nom du fichier</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Date de création</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Expire dans</th>
|
||||
<th scope="col" class="relative px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for file in files %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<i class="bi bi-file-earmark-text-fill mr-1 text-gray-500 dark:text-gray-400"></i> {{ file.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
|
||||
{{ file.createAt|date("d/m/Y H:i:s") }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100">
|
||||
{{ file.expires_in }} minutes
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-center">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<a target="_blank" href="{{ path('artemis_tools_share_view', {'id': file.id}) }}" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-900" title="Voir le fichier">
|
||||
<i class="bi bi-eye-fill mr-1"></i> Voir
|
||||
</a>
|
||||
|
||||
<button type="button" onclick="document.getElementById('qrCodeModal{{ file.id }}').classList.remove('hidden')" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-teal-500 hover:bg-teal-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 dark:focus:ring-offset-gray-900" title="Voir le QR code">
|
||||
<i class="bi bi-qr-code-scan mr-1"></i> QR Code
|
||||
</button>
|
||||
|
||||
<form method="POST" action="{{ path('artemis_tools_share_delete', {'id': file.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce fichier ?');" class="inline">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<button type="submit" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-offset-gray-900" title="Supprimer le fichier">
|
||||
<i class="bi bi-trash-fill mr-1"></i> Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Modals pour les QR Codes (sans changement) #}
|
||||
{% for file in files %}
|
||||
<div id="qrCodeModal{{ file.id }}" class="fixed z-10 inset-0 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
|
||||
<div class="fixed inset-0 bg-gray-900 bg-opacity-75 transition-opacity" aria-hidden="true" onclick="document.getElementById('qrCodeModal{{ file.id }}').classList.add('hidden')"></div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<div class="position-relative inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full">
|
||||
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100" id="modal-title">
|
||||
QR Code pour <strong>{{ file.name }}</strong>
|
||||
</h3>
|
||||
<div class="mt-2 text-center">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Scannez pour accéder au fichier</p>
|
||||
<img src="{{ path('artemis_tools_share_qrcode', {'id': file.id}) }}" alt="QR Code du fichier {{ file.name }}" class="zc mx-auto border dark:border-gray-700 p-2 w-40 h-40 object-contain">
|
||||
<p class="mt-3 text-xs text-gray-400 dark:text-gray-500 truncate">{{ url('artemis_tools_share_view', {'id': file.id}) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-700 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="document.getElementById('qrCodeModal{{ file.id }}').classList.add('hidden')">
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,33 @@
|
||||
<h2 class="text-3xl font-extrabold text-gray-900 text-center">
|
||||
Détails de la Commande et Contact
|
||||
</h2>
|
||||
|
||||
{# --- BLOC D'AFFICHAGE DE LA FORMULE SÉLECTIONNÉE --- #}
|
||||
{% if currentFormule is defined and currentFormule is not empty %}
|
||||
<div class="border-2 border-indigo-600 bg-indigo-50 p-6 rounded-lg shadow-md mb-8">
|
||||
<h3 class="text-xl font-bold text-indigo-800 flex items-center mb-4">
|
||||
<svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm-3.707-9.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0l5-5a1 1 0 10-1.414-1.414L10 13.586l-3.293-3.293z" clip-rule="evenodd"></path></svg>
|
||||
Votre Formule Sélectionnée : <strong>{{ currentFormule.name|default('Formule Inconnue') }}</strong>
|
||||
</h3>
|
||||
<div class="flex justify-between items-center border-t border-indigo-200 pt-3">
|
||||
<p class="text-gray-700 font-medium">Prix :</p>
|
||||
<p class="text-2xl font-extrabold text-indigo-700">
|
||||
{{ currentFormule.price|default('0') }} {{ currentFormule.period|default('€ H.T. / mois') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-2">
|
||||
<p class="text-gray-600 font-medium">Engagement :</p>
|
||||
<p class="text-base text-gray-600">
|
||||
{{ currentFormule.commitment|default('Sans engagement') }}
|
||||
</p>
|
||||
</div>
|
||||
{% if currentFormule.audience is defined %}
|
||||
<p class="mt-2 text-sm text-indigo-700 italic">
|
||||
{{ currentFormule.audience }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{# --- FIN BLOC D'AFFICHAGE DE LA FORMULE SÉLECTIONNÉE --- #}
|
||||
{{ form_start(form, {'attr': {'class': 'mt-8 space-y-6'}}) }}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
Reference in New Issue
Block a user