feat: page admin de gestion de la numerotation des commandes

src/Controller/Admin/OrderNumberController.php (nouveau):
- Route /admin/numerotation, accessible ROLE_ROOT uniquement
- index(): affiche le prochain numero via OrderNumberService::preview()
  et les 20 derniers numeros generes (ORDER BY id DESC)
- update(): modifie le prochain numero en creant une entree placeholder
  avec le numero precedent (N-1) marque comme utilise, pour que le
  prochain generate() retourne le numero souhaite
  - Validation du format MM/YYYY-XXXXX via regex
  - Verification que le numero n'existe pas deja
  - Verification que le numero est au minimum 00001

templates/admin/order_number/index.html.twig (nouveau):
- Section "Prochain numero" : affiche le prochain numero en gros (gold)
  avec formulaire pour le modifier (input avec pattern regex,
  placeholder MM/YYYY-XXXXX, explication de l'utilite)
- Section "Derniers numeros generes" : tableau avec numero (font-mono),
  date de creation, statut (Utilise vert / Reserve gris)
- Design glassmorphism (glass, input-glass, btn-gold, glass-dark header)

templates/admin/_layout.html.twig:
- Ajout du lien "Numerotation" dans la sidebar Super Admin avec
  icone hash (#), route app_admin_order_number, style active-danger

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-02 22:23:07 +02:00
parent 85220c6200
commit cacd3ac66c
3 changed files with 173 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Controller\Admin;
use App\Entity\OrderNumber;
use App\Repository\OrderNumberRepository;
use App\Service\OrderNumberService;
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;
#[Route('/admin/numerotation', name: 'app_admin_order_number')]
#[IsGranted('ROLE_ROOT')]
class OrderNumberController extends AbstractController
{
#[Route('', name: '')]
public function index(OrderNumberService $orderNumberService, OrderNumberRepository $repository): Response
{
$nextNumber = $orderNumberService->preview();
$lastOrders = $repository->createQueryBuilder('o')
->orderBy('o.id', 'DESC')
->setMaxResults(20)
->getQuery()
->getResult();
return $this->render('admin/order_number/index.html.twig', [
'nextNumber' => $nextNumber,
'lastOrders' => $lastOrders,
]);
}
#[Route('/update', name: '_update', methods: ['POST'])]
public function update(Request $request, OrderNumberRepository $repository, EntityManagerInterface $em): Response
{
$newNumber = trim($request->request->getString('next_number'));
if ('' === $newNumber || !preg_match('/^\d{2}\/\d{4}-\d{5}$/', $newNumber)) {
$this->addFlash('error', 'Format invalide. Utilisez le format MM/YYYY-XXXXX (ex: 04/2026-00042).');
return $this->redirectToRoute('app_admin_order_number');
}
// Verifier si ce numero existe deja
$existing = $repository->findOneBy(['numOrder' => $newNumber]);
if (null !== $existing) {
$this->addFlash('error', 'Le numero '.$newNumber.' existe deja.');
return $this->redirectToRoute('app_admin_order_number');
}
// Extraire le prefix et le compteur
$parts = explode('-', $newNumber);
$prefix = $parts[0].'-';
$targetNum = (int) $parts[1];
// On cree l'entree precedente pour que le prochain generate() retourne le bon numero
$previousNum = $targetNum - 1;
if ($previousNum < 0) {
$this->addFlash('error', 'Le numero doit etre au minimum 00001.');
return $this->redirectToRoute('app_admin_order_number');
}
$previousNumOrder = $prefix.str_pad((string) $previousNum, 5, '0', \STR_PAD_LEFT);
// Verifier si l'entree precedente existe deja
$existingPrevious = $repository->findOneBy(['numOrder' => $previousNumOrder]);
if (null === $existingPrevious && $previousNum > 0) {
$placeholder = new OrderNumber($previousNumOrder);
$placeholder->markAsUsed();
$em->persist($placeholder);
$em->flush();
}
$this->addFlash('success', 'Prochain numero de commande mis a jour : '.$newNumber);
return $this->redirectToRoute('app_admin_order_number');
}
}

View File

@@ -77,6 +77,10 @@
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Synchronisation
</a>
<a href="{{ path('app_admin_order_number') }}" class="sidebar-nav-item {{ current_route starts with 'app_admin_order_number' ? 'active-danger' : '' }}" style="color: {{ current_route starts with 'app_admin_order_number' ? 'white' : 'rgba(248,113,113,0.7)' }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/></svg>
Numerotation
</a>
</div>
{% endif %}
</nav>

View File

@@ -0,0 +1,86 @@
{% extends 'admin/_layout.html.twig' %}
{% block title %}Numerotation - Administration - CRM SITECONSEIL{% endblock %}
{% block admin_content %}
<div class="page-container">
<h1 class="text-2xl font-bold heading-page mb-8">Numerotation des commandes</h1>
{% for type, messages in app.flashes %}
{% for message in messages %}
<div class="mb-6 p-4 glass font-medium text-sm rounded-xl" style="border-color: {{ type == 'success' ? 'rgba(34,197,94,0.3)' : 'rgba(220,38,38,0.3)' }}; color: {{ type == 'success' ? '#166534' : '#991b1b' }};">
{{ message }}
</div>
{% endfor %}
{% endfor %}
<div class="flex flex-col gap-8">
<section>
<h2 class="text-xl font-bold uppercase mb-4">Prochain numero</h2>
<div class="glass p-6">
<div class="flex items-center gap-4 mb-6">
<div>
<p class="text-xs font-bold uppercase tracking-wider text-gray-500 mb-1">Prochain numero genere</p>
<p class="text-3xl font-bold text-[#fabf04]">{{ nextNumber }}</p>
</div>
</div>
<div class="border-t border-white/20 pt-6">
<h3 class="text-sm font-bold uppercase tracking-wider mb-3">Modifier le prochain numero</h3>
<p class="text-xs text-gray-500 mb-4">Utilisez cette fonction si des factures ont ete creees en dehors du CRM et que vous devez ajuster la numerotation pour eviter les doublons.</p>
<form method="post" action="{{ path('app_admin_order_number_update') }}" class="flex flex-col sm:flex-row items-start sm:items-end gap-3">
<div class="flex-1 w-full sm:w-auto">
<label for="next_number" class="block text-xs font-bold uppercase tracking-wider mb-2 text-gray-600">Prochain numero souhaite</label>
<input type="text" id="next_number" name="next_number" required
value="{{ nextNumber }}"
pattern="\d{2}/\d{4}-\d{5}"
placeholder="MM/YYYY-XXXXX"
class="input-glass w-full sm:w-64 px-4 py-3 text-sm font-medium font-mono">
</div>
<button type="submit" class="btn-gold px-6 py-3 text-sm font-bold uppercase tracking-wider text-gray-900">
Mettre a jour
</button>
</form>
<p class="text-[10px] text-gray-400 mt-2">Format : MM/YYYY-XXXXX (ex: 04/2026-00042)</p>
</div>
</div>
</section>
<section>
<h2 class="text-xl font-bold uppercase mb-4">Derniers numeros generes</h2>
<div class="glass overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark" style="border-radius: 0;">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-wider text-white/80">Numero</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-wider text-white/80">Date de creation</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-wider text-white/80">Statut</th>
</tr>
</thead>
<tbody>
{% for order in lastOrders %}
<tr class="border-b border-white/10 hover:bg-white/30 transition-colors">
<td class="px-4 py-3 font-mono font-bold">{{ order.numOrder }}</td>
<td class="px-4 py-3 text-gray-500">{{ order.createdAt|date('d/m/Y H:i:s') }}</td>
<td class="px-4 py-3 text-center">
{% if order.used %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Utilise</span>
{% else %}
<span class="px-2 py-0.5 bg-gray-500/20 text-gray-600 font-bold uppercase text-[10px] rounded">Reserve</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="px-4 py-8 text-center text-gray-400 font-bold">Aucun numero genere.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
</div>
{% endblock %}