feat: enregistrement paiement manuel sur avis de paiement

- Bouton "Enregistrer paiement" sur les avis au state 'send'
- Modal avec montant, methode (virement/cheque/especes/CB externe/autre),
  reference optionnelle
- Route manualPayment : cree AdvertPayment, passe avis en accepted,
  genere facture payee, indexe Meilisearch
- Methodes : virement, cheque, especes, CB terminal externe, autre

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-08 19:40:05 +02:00
parent f0bdc60b99
commit 49a4cf3ec2
2 changed files with 110 additions and 0 deletions

View File

@@ -268,6 +268,74 @@ class AdvertController extends AbstractController
]); ]);
} }
/**
* Enregistre un paiement manuel (virement, cheque, especes, etc.).
*/
#[Route('/{id}/manual-payment', name: 'manual_payment', requirements: ['id' => '\d+'], methods: ['POST'])]
public function manualPayment(
int $id,
\Symfony\Component\HttpFoundation\Request $request,
FactureService $factureService,
): Response {
$advert = $this->em->getRepository(Advert::class)->find($id);
if (null === $advert) {
throw $this->createNotFoundException(self::MSG_NOT_FOUND);
}
$amount = $request->request->getString('amount');
$method = $request->request->getString('method');
$reference = trim($request->request->getString('reference'));
if ('' === $amount || '' === $method) {
$this->addFlash('error', 'Montant et methode de paiement requis.');
return $this->redirectToRoute('app_admin_clients_show', [
'id' => $advert->getCustomer()?->getId() ?? 0,
'tab' => 'avis',
]);
}
$methodLabel = match ($method) {
'virement' => 'Virement bancaire',
'cheque' => 'Cheque',
'especes' => 'Especes',
'cb_externe' => 'CB (terminal externe)',
default => 'Autre',
};
if ('' !== $reference) {
$methodLabel .= ' (Ref: '.$reference.')';
}
// Creer l'AdvertPayment
$payment = new \App\Entity\AdvertPayment($advert, \App\Entity\AdvertPayment::TYPE_SUCCESS, number_format((float) $amount, 2, '.', ''));
$payment->setMethod($method);
$this->em->persist($payment);
// Mettre a jour l'etat de l'avis
$advert->setState(Advert::STATE_ACCEPTED);
// Tracker l'evenement
$this->em->persist(new \App\Entity\AdvertEvent($advert, \App\Entity\AdvertEvent::TYPE_PAY, 'Paiement manuel : '.$methodLabel.' - '.$amount.' EUR'));
$this->em->flush();
// Generer la facture
try {
$factureService->createPaidFactureFromAdvert($advert, number_format((float) $amount, 2, '.', ''), $methodLabel);
} catch (\Throwable $e) {
$this->addFlash('warning', 'Paiement enregistre mais erreur generation facture : '.$e->getMessage());
}
$this->meilisearch->indexAdvert($advert);
$this->addFlash('success', 'Paiement de '.$amount.' EUR enregistre ('.$methodLabel.') pour l\'avis '.$advert->getOrderNumber()->getNumOrder().'.');
return $this->redirectToRoute('app_admin_clients_show', [
'id' => $advert->getCustomer()?->getId() ?? 0,
'tab' => 'avis',
]);
}
#[Route('/{id}/sync-payment', name: 'sync_payment', requirements: ['id' => '\d+'], methods: ['POST'])] #[Route('/{id}/sync-payment', name: 'sync_payment', requirements: ['id' => '\d+'], methods: ['POST'])]
public function syncPayment( public function syncPayment(
int $id, int $id,

View File

@@ -641,6 +641,10 @@
<form method="post" action="{{ path('app_admin_advert_resend', {id: a.id}) }}" class="inline" data-confirm="Renvoyer l'avis de paiement au client ?"> <form method="post" action="{{ path('app_admin_advert_resend', {id: a.id}) }}" class="inline" data-confirm="Renvoyer l'avis de paiement au client ?">
<button type="submit" class="px-3 py-1 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px]transition-all">Renvoyer</button> <button type="submit" class="px-3 py-1 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px]transition-all">Renvoyer</button>
</form> </form>
<button type="button" data-modal-open="modal-manual-pay-{{ a.id }}"
class="px-3 py-1 bg-green-500/20 text-green-700 hover:bg-green-500 hover:text-white font-bold uppercase text-[10px] transition-all">
Enregistrer paiement
</button>
{% endif %} {% endif %}
{% if a.state == 'accepted' and a.factures|length == 0 %} {% if a.state == 'accepted' and a.factures|length == 0 %}
<form method="post" action="{{ path('app_admin_advert_create_facture', {id: a.id}) }}" class="inline" data-confirm="Creer la facture pour l'avis {{ a.orderNumber.numOrder }} ?"> <form method="post" action="{{ path('app_admin_advert_create_facture', {id: a.id}) }}" class="inline" data-confirm="Creer la facture pour l'avis {{ a.orderNumber.numOrder }} ?">
@@ -682,6 +686,44 @@
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun avis de paiement.</div> <div class="glass p-8 text-center text-gray-400 font-bold">Aucun avis de paiement.</div>
{% endif %} {% endif %}
{# Modals paiement manuel #}
{% for a in advertsList %}
{% if a.state == 'send' %}
<div id="modal-manual-pay-{{ a.id }}" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="glass-heavy p-6 w-full max-w-md">
<h2 class="text-lg font-bold uppercase mb-4">Enregistrer un paiement</h2>
<p class="text-xs text-gray-500 mb-4">Avis {{ a.orderNumber.numOrder }} - {{ a.totalTtc }} &euro;</p>
<form method="post" action="{{ path('app_admin_advert_manual_payment', {id: a.id}) }}">
<div class="grid grid-cols-1 gap-3 mb-4">
<div>
<label for="mp-amount-{{ a.id }}" class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Montant recu</label>
<input type="number" id="mp-amount-{{ a.id }}" name="amount" step="0.01" min="0.01" value="{{ a.totalTtc }}" required class="input-glass w-full px-3 py-2 text-xs font-bold">
</div>
<div>
<label for="mp-method-{{ a.id }}" class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Methode de paiement</label>
<select id="mp-method-{{ a.id }}" name="method" required class="input-glass w-full px-3 py-2 text-xs font-bold">
<option value="virement">Virement bancaire</option>
<option value="cheque">Cheque</option>
<option value="especes">Especes</option>
<option value="cb_externe">CB (terminal externe)</option>
<option value="autre">Autre</option>
</select>
</div>
<div>
<label for="mp-reference-{{ a.id }}" class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-1">Reference (optionnel)</label>
<input type="text" id="mp-reference-{{ a.id }}" name="reference" placeholder="N de virement, n de cheque..." class="input-glass w-full px-3 py-2 text-xs font-bold">
</div>
</div>
<div class="flex justify-end gap-2">
<button type="button" data-modal-close="modal-manual-pay-{{ a.id }}" class="px-4 py-2 glass font-bold uppercase text-[10px] tracking-widest">Annuler</button>
<button type="submit" class="px-4 py-2 bg-green-600 text-white font-bold uppercase text-[10px] tracking-wider hover:bg-green-700 transition-all">Enregistrer</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endfor %}
{# Tab: Devis #} {# Tab: Devis #}
{% elseif tab == 'devis' %} {% elseif tab == 'devis' %}
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">