feat(CustomerController): Ajoute création de devis client.

Ajoute la possibilité de créer des devis pour un client donné, avec
gestion des numéros de devis et des lignes de devis.
This commit is contained in:
Serreau Jovann
2025-07-24 10:08:51 +02:00
parent 6476186275
commit 38d1fca150
11 changed files with 336 additions and 18 deletions

View File

@@ -4,6 +4,7 @@ import {AutoSubmit} from './class/AutoSubmit'
import {ServerCard} from './class/ServerCard'
import {AutoCustomer} from './class/AutoCustomer'
import {RepeatLine} from './class/RepeatLine'
import {OrderCtrl} from './class/OrderCtrl'
function script() {
@@ -11,6 +12,7 @@ function script() {
customElements.define('server-card',ServerCard,{extends:'div'})
customElements.define('auto-customer',AutoCustomer,{extends:'button'})
customElements.define('repeat-line',RepeatLine,{extends:'div'})
customElements.define('order-ctrl',OrderCtrl,{extends:'div'})
}

23
assets/class/OrderCtrl.js Normal file
View File

@@ -0,0 +1,23 @@
export class OrderCtrl extends HTMLDivElement{
connectedCallback(){
let element = this;
let selectType = this.querySelector('select[name="type"]');
let inputNum = this.querySelector('input[name="num"]');
fetch("/api-interne/intranet/customer/order/num?type=avis")
.then(response => response.json())
.then(data => {
inputNum.setAttribute('value',data.num)
inputNum.value = data.num;
})
selectType.addEventListener('change', (event)=>{
fetch("/api-interne/intranet/customer/order/num?type="+event.target.value)
.then(response => response.json())
.then(data => {
inputNum.setAttribute('value',data.num)
inputNum.value = data.num;
})
})
}
}

View 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 Version20250724074150 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 order_number_current (id SERIAL NOT NULL, current_number VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE order_number_dispo (id SERIAL NOT NULL, num VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
}
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 order_number_current');
$this->addSql('DROP TABLE order_number_dispo');
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Controller\ApiInterne\Intranet;
use App\Entity\CustomerAdvertPayment;
use App\Entity\OrderNumberCurrent;
use App\Repository\CustomerAdvertPaymentRepository;
use App\Repository\CustomerDevisLineRepository;
use App\Repository\CustomerOrderRepository;
use App\Repository\OrderNumberCurrentRepository;
use App\Repository\OrderNumberDispoRepository;
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;
class NumController extends AbstractController
{
#[Route(path: '/api-interne/intranet/customer/order/num',name: 'api-interne-intranet-customer-order-num')]
public function orderNul(Request $request,
CustomerAdvertPaymentRepository $customerAdvertPaymentRepository,
CustomerDevisLineRepository $customerDevisLineRepository,
CustomerOrderRepository $customerOrderRepository,
OrderNumberCurrentRepository $currentRepository,
OrderNumberDispoRepository $currentDispoRepository,
EntityManagerInterface $entityManager,
): Response
{
$type = $request->query->get('type');
$t = new \DateTimeImmutable();
if($type == "avis"){
return $this->json([
'num' => "A-".$t->format('Y/m')."/".sprintf('%05d',$customerAdvertPaymentRepository->count()+1),
]);
} elseif ($type == "devis") {
return $this->json([
'num' => "D-".$t->format('Y/m')."/".sprintf('%05d',$customerDevisLineRepository->count()+1),
]);
} else {
$numFinal = null;
if($currentDispoRepository->count() >0) {
$numFinal = $currentDispoRepository->findBy([],['id'=>'ASC'])[0]->getNum();
} else {
$lastNum = $currentRepository->find(1);
if(!$lastNum instanceof OrderNumberCurrent) {
$lastNum = new OrderNumberCurrent();
$lastNum->setCurrentNumber($customerOrderRepository->count());
$numFinal = "F-".$t->format('Y/m')."/".sprintf('%05d',$customerOrderRepository->count()+1);
$entityManager->persist($lastNum);
$entityManager->flush();
} else {
$numFinal = "F-".$t->format('Y/m')."/".sprintf('%05d',$lastNum->getCurrentNumber()+1);
}
}
return $this->json([
'num' => $numFinal,
]);
}
}
}

View File

@@ -3,7 +3,10 @@
namespace App\Controller\Artemis\Intranet;
use App\Entity\Customer;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerContact;
use App\Entity\CustomerDevis;
use App\Entity\CustomerDevisLine;
use App\Entity\CustomerDns;
use App\Form\Artemis\Intranet\CustomerEditType;
use App\Form\Artemis\Intranet\CustomerNddType;
@@ -126,10 +129,15 @@ class CustomerController extends AbstractController
}
return $this->redirectToRoute('artemis_intranet_customer_view',['id'=>$customer->getId(),'current'=>'nnd']);
}
$orderDevis = $entityManager->getRepository(CustomerDevis::class)->findBy(['customer'=>$customer],['id'=>'ASC']);
return $this->render('artemis/intranet/customer/edit.twig',[
'form' => $form->createView(),
'formNdd' => $formNdd->createView(),
'customer' => $customer,
'orderDevis' => $orderDevis,
'current' => $request->get('current','main'),
'currentOrder' => $request->get('currentOrder','f'),
'ndds' => $customerDnsRepository->findBy(['customer'=>$customer],['expiredAt'=>'asc'])
@@ -173,6 +181,34 @@ class CustomerController extends AbstractController
#[Route(path: '/artemis/intranet/customer/{id}/orderAdd',name: 'artemis_intranet_customer_orderAdd',methods: ['GET', 'POST'])]
public function customerOrder(?Customer $customer,Request $request,LoggerService $loggerService,EntityManagerInterface $entityManager,EventDispatcherInterface $eventDispatcher): Response
{
if($request->isMethod('POST')) {
$data = $_POST;
if($data['type'] == "devis") {
$devis = new CustomerDevis();
$devis->setCustomer($customer);
$devis->setCreateAt(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i',$data['date']));
$devis->setNumDevis($data['num']);
$devis->setState("created");
$entityManager->persist($devis);
$r=0;
foreach ($data['lines'] as $line) {
$devisLine = new CustomerDevisLine();
$devisLine->setPos($r);
$devisLine->setName($line['title']);
$devisLine->setPriceHT($line['price']);
$devisLine->setContent($line['description']);
$devisLine->setTva(1.20);
$entityManager->persist($devisLine);
$devis->addCustomerDevisLine($devisLine);
$entityManager->persist($devis);
$r = $r+1;
}
$entityManager->flush();
$this->addFlash("success","Création effectuée");
return $this->redirectToRoute('artemis_intranet_customer_view',['id'=>$customer->getId(),'current'=>'order']);
}
}
return $this->render('artemis/intranet/customer/order-add.twig',[
'customer' => $customer,
]);

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Entity;
use App\Repository\OrderNumberCurrentRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderNumberCurrentRepository::class)]
class OrderNumberCurrent
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $currentNumber = null;
public function getId(): ?int
{
return $this->id;
}
public function getCurrentNumber(): ?string
{
return $this->currentNumber;
}
public function setCurrentNumber(string $currentNumber): static
{
$this->currentNumber = $currentNumber;
return $this;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Entity;
use App\Repository\OrderNumberDispoRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderNumberDispoRepository::class)]
class OrderNumberDispo
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $num = null;
public function getId(): ?int
{
return $this->id;
}
public function getNum(): ?string
{
return $this->num;
}
public function setNum(string $num): static
{
$this->num = $num;
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\OrderNumberCurrent;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OrderNumberCurrent>
*/
class OrderNumberCurrentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OrderNumberCurrent::class);
}
// /**
// * @return OrderNumberCurrent[] Returns an array of OrderNumberCurrent objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('o.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?OrderNumberCurrent
// {
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\OrderNumberDispo;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OrderNumberDispo>
*/
class OrderNumberDispoRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OrderNumberDispo::class);
}
// /**
// * @return OrderNumberDispo[] Returns an array of OrderNumberDispo objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('o.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?OrderNumberDispo
// {
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -6,7 +6,7 @@
</div>
<form method="post" class="mt-5 bg-gray-800 rounded-lg shadow-lg p-6 space-y-4">
<div class="flex space-x-4">
<div class="flex space-x-4" is="order-ctrl">
<div class="flex-1">
<div class="mb-5">
<label for="type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Type</label>
@@ -34,7 +34,7 @@
<fieldset class="form-section">
<legend>
<h2>Guests</h2>
<h2>Ligne(s)</h2>
</legend>
<div class="form-repeater" data-component="repeater" is="repeat-line">
@@ -47,11 +47,11 @@
<div class="form-field">
<div class="mb-1">
<label for="titre" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Titre</label>
<input type="text" name="titre" id="lines[0]['title]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
<input type="text" name="lines[0][title]" id="lines[0][title]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
<div class="mb-1">
<label for="price" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Prix TTC</label>
<input type="number" step="0.1" name="titre" id="lines[0]['price]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
<input type="number" step="0.1" name="lines[0][price]" id="lines[0][price]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
<button
class="w-full form-repeater__remove-button bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
@@ -66,7 +66,7 @@
<div class="form-field">
<div class="mb-5">
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea rows="7" type="text" name="description" id="lines[0]['description]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required></textarea>
<textarea rows="7" type="text" name="lines[0][description]" id="lines[0][description]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required></textarea>
</div>
</div>
</div>
@@ -89,7 +89,7 @@
<button
class="w-full bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded"
type="button"
type="submit"
>
Enregistrer
</button>

View File

@@ -58,18 +58,20 @@
</tr>
</thead>
<tbody>
<tr class="hover:bg-gray-700">
<td class="px-6 py-4">Facture</td>
<td class="px-6 py-4">F2025-001</td>
<td class="px-6 py-4">2025-07-01</td>
<td class="px-6 py-4">1 200,00 €</td>
<td class="px-6 py-4 text-green-400">Payé</td>
<td class="px-6 py-4 text-center">
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Modifier</button>
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Télécharger</button>
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Annulée</button>
</td>
</tr>
{% for orderDevi in orderDevis %}
<tr class="hover:bg-gray-700">
<td class="px-6 py-4">DEVIS</td>
<td class="px-6 py-4">{{ orderDevi.numDevis }}</td>
<td class="px-6 py-4">{{ orderDevi.createAt|date('d/m/Y H:i') }}</td>
<td class="px-6 py-4">1 200,00 €</td>
<td class="px-6 py-4 text-green-400">Payé</td>
<td class="px-6 py-4 text-center">
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Modifier</button>
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Télécharger</button>
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Annulée</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>