```
✨ 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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
/vendor/
|
||||
/public/storage/
|
||||
/public/tmp/*.pdf
|
||||
/public/images/Catalogue.pdf
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
|
||||
@@ -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
@@ -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'])]
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
|
||||
{% block actions %}
|
||||
<div class="flex items-center space-x-3">
|
||||
{# 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" />
|
||||
@@ -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">
|
||||
<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">
|
||||
{# 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"></path>
|
||||
</svg>
|
||||
<span>Nouvelle options</span>
|
||||
<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>
|
||||
</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,10 +124,8 @@
|
||||
<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">
|
||||
<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 %}
|
||||
@@ -164,66 +134,67 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
.custom-pagination nav ul { @apply flex space-x-2; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user