feat(gitignore): Ajoute Catalogue.pdf aux fichiers ignorés.
🎨 style(templates): Ajoute un lien vers le catalogue PDF dans la page produits.
♻️ refactor(pwa): Met à jour l'URL du catalogue PDF dans le fichier PWA.
♻️ refactor(templates): Met à jour l'URL du catalogue PDF dans la base de réservation.
 feat(ProductController): Ajoute une route pour mettre à jour le catalogue PDF.
```
This commit is contained in:
Serreau Jovann
2026-01-28 14:11:57 +01:00
parent e863ccf790
commit 6362f389b4
6 changed files with 103 additions and 147247 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@
/vendor/
/public/storage/
/public/tmp/*.pdf
/public/images/Catalogue.pdf
###< symfony/framework-bundle ###
###> phpunit/phpunit ###

View File

@@ -61,7 +61,7 @@ pwa:
- name: "Catalogue"
short_name: "Catalogue"
description: "Catalogue"
url: "/provider/Catalogue.pdf"
url: "/images/Catalogue.pdf"
icons:
- src: "%kernel.project_dir%/public/provider/images/favicon.png"
sizes: [ 96 ]

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,8 @@ use App\Service\Stripe\Client;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -67,6 +69,36 @@ class ProductController extends AbstractController
]);
}
#[Route(path: '/crm/catalogue/upload', name: 'app_crm_catalogue_upload', methods: ['POST'])]
public function uploadCatalogue(Request $request, AppLogger $logger): Response
{
/** @var UploadedFile $file */
$file = $request->files->get('catalogue_pdf');
if ($file) {
// Vérification de sécurité (extension)
if ($file->guessExtension() !== 'pdf') {
$this->addFlash('error', 'Le fichier doit être un PDF valide.');
return $this->redirectToRoute('app_crm_product');
}
try {
// Chemin vers le dossier public/images
$destination = $this->getParameter('kernel.project_dir') . '/public/images';
// On force le nom "Catalogue.pdf" pour écraser l'existant
$file->move($destination, 'Catalogue.pdf');
$logger->record('UPDATE', 'Mise à jour du fichier Catalogue.pdf');
$this->addFlash('success', 'Le catalogue PDF a été mis à jour avec succès.');
} catch (FileException $e) {
$this->addFlash('error', 'Une erreur est survenue lors de la sauvegarde du fichier.');
}
}
return $this->redirectToRoute('app_crm_product');
}
// --- PRODUITS (ADD/EDIT/DELETE) ---
#[Route(path: '/crm/products/add', name: 'app_crm_product_add', methods: ['GET', 'POST'])]

View File

@@ -5,7 +5,15 @@
{% block actions %}
<div class="flex items-center space-x-3">
<a data-turbo="false" href="{{ path('app_crm_product_add') }}" class="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
{# LIEN VERS LE CATALOGUE PDF EXISTANT #}
<a href="/images/Catalogue.pdf" target="_blank" class="flex items-center space-x-2 px-5 py-3 bg-white/5 hover:bg-white/10 text-slate-300 text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all border border-white/10 group">
<svg class="w-4 h-4 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<span>Voir le Catalogue PDF</span>
</a>
<a data-turbo="false" href="{{ path('app_crm_product_add') }}" class="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4" />
</svg>
@@ -15,6 +23,7 @@
{% endblock %}
{% block body %}
{# SECTION PRODUITS #}
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] overflow-hidden shadow-2xl animate-in fade-in duration-700">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
@@ -31,7 +40,6 @@
<tbody class="divide-y divide-white/5">
{% for product in products %}
<tr class="group hover:bg-white/[0.02] transition-colors">
{# VISUEL & REF #}
<td class="px-6 py-4">
<div class="flex items-center space-x-4">
<div class="h-12 w-12 rounded-xl overflow-hidden border border-white/10 bg-slate-900 flex-shrink-0">
@@ -46,22 +54,12 @@
<span class="text-[10px] font-mono font-bold text-blue-500 tracking-wider">{{ product.ref }}</span>
</div>
</td>
{# NOM #}
<td class="px-6 py-4">
<span class="text-sm font-bold text-white group-hover:text-blue-400 transition-colors capitalize">
{{ product.name }}
</span>
<span class="text-sm font-bold text-white group-hover:text-blue-400 transition-colors capitalize">{{ product.name }}</span>
</td>
{# CATEGORIE #}
<td class="px-6 py-4">
<span class="px-3 py-1 bg-white/5 border border-white/10 rounded-lg text-[9px] font-black text-slate-400 uppercase tracking-widest">
{{ product.category }}
</span>
<span class="px-3 py-1 bg-white/5 border border-white/10 rounded-lg text-[9px] font-black text-slate-400 uppercase tracking-widest">{{ product.category }}</span>
</td>
{# STRIPE #}
<td class="px-6 py-4">
{% if product.productId %}
<div class="flex items-center text-[8px] font-black text-emerald-400 uppercase tracking-widest">
@@ -75,69 +73,43 @@
</div>
{% endif %}
</td>
{# PRIX #}
<td class="px-6 py-4 text-center">
<span class="text-sm font-black text-emerald-400">
{{ product.priceDay|number_format(2, ',', ' ') }}
</span>
<span class="text-sm font-black text-emerald-400">{{ product.priceDay|number_format(2, ',', ' ') }}€</span>
</td>
{# ACTIONS #}
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-2">
<a data-turbo="false" href="{{ path('app_crm_product_edit', {id: product.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5">
<a data-turbo="false" href="{{ path('app_crm_product_edit', {id: product.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
</a>
<a data-turbo="false" href="{{ path('app_crm_product_delete', {id: product.id}) }}?_token={{ csrf_token('delete' ~ product.id) }}"
data-turbo-method="post"
data-turbo-confirm="Confirmer la suppression définitive de '{{ product.name }}' ?"
class="p-2 bg-rose-500/10 hover:bg-rose-500 text-rose-500 hover:text-white rounded-xl transition-all border border-rose-500/20 shadow-lg shadow-rose-500/5">
<a data-turbo="false" href="{{ path('app_crm_product_delete', {id: product.id}) }}?_token={{ csrf_token('delete' ~ product.id) }}" data-turbo-method="post" data-turbo-confirm="Confirmer la suppression ?" class="p-2 bg-rose-500/10 hover:bg-rose-500 text-rose-500 hover:text-white rounded-xl transition-all border border-rose-500/20 shadow-lg shadow-rose-500/5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="py-24 text-center">
<p class="text-slate-500 italic uppercase tracking-[0.2em] text-[10px] font-black">Aucun produit dans le catalogue</p>
</td>
</tr>
<tr><td colspan="6" class="py-24 text-center italic text-slate-500 text-[10px] font-black uppercase tracking-[0.2em]">Aucun produit</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# PAGINATION #}
{# PAGINATION PRODUITS #}
{% if products.getTotalItemCount is defined and products.getTotalItemCount > products.getItemNumberPerPage %}
<div class="mt-8 flex justify-center custom-pagination">
{{ knp_pagination_render(products) }}
</div>
<div class="mt-8 flex justify-center custom-pagination">{{ knp_pagination_render(products) }}</div>
{% endif %}
<div class="mt-5 flex items-end justify-between mb-10 pb-8 border-b border-slate-200 dark:border-slate-800/50">
<div>
<h1 class="text-4xl font-extrabold text-slate-900 dark:text-white">
Gestion des <span class="text-blue-500">Options</span>
</h1>
</div>
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-3">
<a data-turbo="false" href="{{ path('app_crm_product_options_add') }}" class="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4"></path>
</svg>
<span>Nouvelle options</span>
</a>
</div>
</div>
{# SECTION OPTIONS #}
<div class="mt-16 flex items-end justify-between mb-8 pb-6 border-b border-white/5">
<h2 class="text-4xl font-extrabold text-white">Gestion des <span class="text-blue-500">Options</span></h2>
<a data-turbo="false" href="{{ path('app_crm_product_options_add') }}" class="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20 group">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4" /></svg>
<span>Nouvelle Option</span>
</a>
</div>
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] overflow-hidden shadow-2xl animate-in fade-in duration-700">
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] overflow-hidden shadow-2xl">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
@@ -152,78 +124,77 @@
<tbody class="divide-y divide-white/5">
{% for option in options %}
<tr class="group hover:bg-white/[0.02] transition-colors">
{# VISUEL & REF #}
<td class="px-6 py-4">
<div class="flex items-center space-x-4">
<div class="h-12 w-12 rounded-xl overflow-hidden border border-white/10 bg-slate-900 flex-shrink-0">
{% if option.imageName %}
<img src="{{ vich_uploader_asset(option, 'imageFile') | imagine_filter('webp') }}" class="h-full w-full object-cover">
{% else %}
<div class="h-full w-full flex items-center justify-center bg-slate-800 text-slate-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
{% endif %}
</div>
<div class="h-12 w-12 rounded-xl overflow-hidden border border-white/10 bg-slate-900">
{% if option.imageName %}
<img src="{{ vich_uploader_asset(option, 'imageFile') | imagine_filter('webp') }}" class="h-full w-full object-cover">
{% else %}
<div class="h-full w-full flex items-center justify-center bg-slate-800 text-slate-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
{% endif %}
</div>
</td>
<td class="px-6 py-4">
<span class="text-sm font-bold text-white group-hover:text-blue-400 transition-colors capitalize">
{{ option.name }}
</span>
<span class="text-sm font-bold text-white group-hover:text-blue-400 transition-colors capitalize">{{ option.name }}</span>
</td>
<td class="px-6 py-4">
{% if option.stripeId != "" %}
<div class="flex items-center text-[8px] font-black text-emerald-400 uppercase tracking-widest">
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500 mr-2 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></span>
Synchronisé
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500 mr-2"></span> Synchronisé
</div>
{% else %}
<div class="flex items-center text-[8px] font-black text-rose-500 uppercase tracking-widest opacity-60">
<span class="h-1.5 w-1.5 rounded-full bg-rose-500 mr-2 animate-pulse"></span>
En attente
<span class="h-1.5 w-1.5 rounded-full bg-rose-500 mr-2 animate-pulse"></span> En attente
</div>
{% endif %}
</td>
{# PRIX #}
<td class="px-6 py-4 text-center">
<span class="text-sm font-black text-emerald-400">
{{ option.priceHt|number_format(2, ',', ' ') }}
</span>
<span class="text-sm font-black text-emerald-400">{{ option.priceHt|number_format(2, ',', ' ') }}€</span>
</td>
{# ACTIONS #}
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-2">
<a data-turbo="false" href="{{ path('app_crm_product_options_edit', {id: option.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
</a>
<a data-turbo="false" href="{{ path('app_crm_product_option_delete', {id: option.id}) }}?_token={{ csrf_token('delete' ~ option.id) }}"
data-turbo-method="post"
data-turbo-confirm="Confirmer la suppression définitive de '{{ option.name }}' ?"
class="p-2 bg-rose-500/10 hover:bg-rose-500 text-rose-500 hover:text-white rounded-xl transition-all border border-rose-500/20 shadow-lg shadow-rose-500/5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</a>
<a data-turbo="false" href="{{ path('app_crm_product_options_edit', {id: option.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg></a>
<a data-turbo="false" href="{{ path('app_crm_product_option_delete', {id: option.id}) }}?_token={{ csrf_token('delete' ~ option.id) }}" data-turbo-method="post" data-turbo-confirm="Supprimer ?" class="p-2 bg-rose-500/10 hover:bg-rose-500 text-rose-500 hover:text-white rounded-xl transition-all border border-rose-500/20"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg></a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="py-24 text-center">
<p class="text-slate-500 italic uppercase tracking-[0.2em] text-[10px] font-black">Aucun options dans le catalogue</p>
</td>
</tr>
<tr><td colspan="5" class="py-24 text-center italic text-slate-500 text-[10px] font-black uppercase tracking-[0.2em]">Aucune option</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# PAGINATION #}
{% if options.getTotalItemCount is defined and options.getTotalItemCount > options.getItemNumberPerPage %}
<div class="mt-8 flex justify-center custom-pagination">
{{ knp_pagination_render(options) }}
{# SECTION MISE À JOUR CATALOGUE PDF (BOT) #}
<div class="mt-20">
<div class="mb-6">
<h3 class="text-2xl font-extrabold text-white flex items-center space-x-3">
<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>Mise à jour du <span class="text-blue-500">catalogue PDF</span></span>
</h3>
<p class="text-slate-400 text-xs mt-2 uppercase tracking-widest font-bold">Remplacez le fichier PDF accessible par les clients</p>
</div>
{% endif %}
<form data-turbo="false" action="{{ path('app_crm_catalogue_upload') }}" method="POST" enctype="multipart/form-data" class="relative group">
<div class="flex items-center space-x-4 p-4 bg-[#1e293b]/60 border border-white/10 rounded-2xl hover:border-blue-500/30 transition-all">
<div class="flex-grow">
<input type="file" name="catalogue_pdf" accept="application/pdf" required
class="block w-full text-[10px] text-slate-400 font-bold uppercase tracking-widest
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0
file:text-[10px] file:font-black file:uppercase file:tracking-widest
file:bg-blue-600/20 file:text-blue-400 file:cursor-pointer
hover:file:bg-blue-600/30 transition-all">
</div>
<button type="submit" class="px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-xl transition-all shadow-lg shadow-blue-600/20">
Mettre à jour
</button>
</div>
</form>
</div>
<style>
.custom-pagination nav ul { @apply flex space-x-2; }

View File

@@ -111,7 +111,7 @@
<a href="{{ path('reservation') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Accueil</a>
<a href="{{ path('reservation_catalogue') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Nos structures </a>
<a href="{{ path('reservation_formules') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Nos Formules </a>
<a target="_blank" href="/provider/Catalogue.pdf" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Catalogue</a>
<a target="_blank" href="/images/Catalogue.pdf" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Catalogue</a>
<a href="{{ path('reservation_workflow') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Comment Reserver</a>
<a href="{{ path('reservation_contact') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Contact</a>