feat(vich_uploader): Ajoute la configuration VichUploader pour les factures.

 feat(order/f.twig): Affiche les factures du client avec actions.
 feat(CustomerController): Gère l'affichage et l'envoi des factures.
🆕 feat(FacturePdf): Crée un service PDF pour les factures clients.
🆕 feat(ContactListType): Ajoute un formulaire pour créer une liste de contacts.
🆕 feat(ContactController): Gère les listes de contacts pour la newsletter.
 feat(base.twig): Ajoute un menu pour la gestion de la newsletter.
 feat(CustomerOrder): Ajoute les champs et annotations pour l'upload de facture.
🆕 feat(contact.twig): Affiche la liste des contacts.
🆕 feat(BillingEventSusbriber): Gère la génération de la facture PDF.
🆕 feat(TemplateController): Initialise le controller des templates de newsletter.
🆕 feat(CompaignController): Crée un controller pour les campagnes newsletter.
🎨 style(admin.scss): Ajoute le style css pour la card contact newsletter.
🆕 feat(add.twig): Ajoute le formulaire de création de liste de contact.
This commit is contained in:
Serreau Jovann
2025-08-01 10:41:05 +02:00
parent 0422f80f2f
commit 14e236da51
19 changed files with 802 additions and 20 deletions

View File

@@ -26,3 +26,13 @@ input {
.bg-opacity-70{
opacity: .7;
}
.card-contact{
border: 1px solid #1a202c;
background: var(--color-gray-600);
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
.flex{
padding: 0.5rem;
}
}

View File

@@ -49,6 +49,14 @@ vich_uploader:
inject_on_load: false
delete_on_update: true
delete_on_remove: true
facture:
uri_prefix: /storage/facture
upload_destination: '%kernel.project_dir%/public/storage/facture'
namer: App\VichUploader\Namer\Customer\DevisName # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Customer\DevisName
inject_on_load: false
delete_on_update: true
delete_on_remove: true
#mappings:
# products:
# uri_prefix: /images/products

View File

@@ -0,0 +1,32 @@
<?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 Version20250731081945 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('ALTER TABLE customer_order ADD state VARCHAR(255) DEFAULT NULL');
}
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('ALTER TABLE customer_order DROP state');
}
}

View File

@@ -0,0 +1,40 @@
<?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 Version20250731082547 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('ALTER TABLE customer_order ADD facture_file_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_order ADD facture_dimensions JSON DEFAULT NULL');
$this->addSql('ALTER TABLE customer_order ADD facture_size VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_order ADD facture_mine_type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer_order ADD facture_original_name VARCHAR(255) DEFAULT NULL');
}
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('ALTER TABLE customer_order DROP facture_file_name');
$this->addSql('ALTER TABLE customer_order DROP facture_dimensions');
$this->addSql('ALTER TABLE customer_order DROP facture_size');
$this->addSql('ALTER TABLE customer_order DROP facture_mine_type');
$this->addSql('ALTER TABLE customer_order DROP facture_original_name');
}
}

View File

@@ -0,0 +1,33 @@
<?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 Version20250801075326 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 contact (id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, uuid UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN contact.uuid IS \'(DC2Type:uuid)\'');
}
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 contact');
}
}

View File

@@ -137,12 +137,14 @@ class CustomerController extends AbstractController
$order->setNumOrder($numFinal);
$order->setAvisPayment($customerAdvertPayment);
$order->setCustomer($customer);
$order->setState("f-created");
$order->setCreateAt(new \DateTimeImmutable());
$order->setUpdateAt(new \DateTimeImmutable());
$entityManager->persist($order);
foreach ($customerAdvertPayment->getCustomerAdvertPaymentLines() as $line) {
$orderLine = new CustomerOrderLine();
$orderLine->setPo($line->getPo());
$orderLine->setPo($line->getPos());
$orderLine->setPriceHt($line->getPriceHt());
$orderLine->setName($line->getName());
$orderLine->setContent($line->getContent());
@@ -276,12 +278,14 @@ class CustomerController extends AbstractController
$orderDevis = $entityManager->getRepository(CustomerDevis::class)->findBy(['customer'=>$customer],['id'=>'ASC']);
$orderAdvert = $entityManager->getRepository(CustomerAdvertPayment::class)->findBy(['customer'=>$customer],['id'=>'ASC']);
$orderOrder = $entityManager->getRepository(CustomerOrder::class)->findBy(['customer'=>$customer],['id'=>'ASC']);
return $this->render('artemis/intranet/customer/edit.twig',[
'form' => $form->createView(),
'formNdd' => $formNdd->createView(),
'customer' => $customer,
'orderDevis' => $paginator->paginate($orderDevis,$request->get('page',1),20),
'orderOrders' => $paginator->paginate($orderOrder,$request->get('page',1),20),
'orderAdverts' => $paginator->paginate($orderAdvert,$request->get('page',1),20),
'current' => $request->get('current','main'),
'currentOrder' => $request->get('currentOrder','f'),

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Controller\Artemis\Newsletter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class CompaignController extends AbstractController
{
#[Route(path: '/artemis/newsletter/campaign',name: 'artemis_newsletter_campaign',methods: ['GET', 'POST'])]
public function artemis(): Response
{
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Controller\Artemis\Newsletter;
use App\Entity\Newsletter\Contact;
use App\Form\Artemis\Newsletter\ContactListType;
use App\Repository\Newsletter\ContactRepository;
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\Uid\Uuid;
class ContactController extends AbstractController
{
#[Route(path: '/artemis/newsletter/contact',name: 'artemis_newsletter_contact',methods: ['GET', 'POST'])]
public function contacts(ContactRepository $contactRepository): Response
{
$r = new Contact();
$r->setUuid(Uuid::v4());
$r->setName("list 1");
// Récupération des contacts triés par id (ordre croissant)
$contacts = $contactRepository->findBy([],['id'=>'ASC']);
// Affiche dans un template Twig (à créer)
return $this->render('artemis/newsletter/contact.twig', [
'lists' => $contacts,
]);
}
#[Route(path: '/artemis/newsletter/contact/add', name: 'artemis_newsletter_contact_add', methods: ['GET', 'POST'])]
public function contactsAdd(Request $request, EntityManagerInterface $em): Response
{
$contactList = new Contact();
$form = $this->createForm(ContactListType::class, $contactList);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$contactList->setUuid(Uuid::v4());
$em->persist($contactList);
$em->flush();
$this->addFlash('success', 'La liste a été créée avec succès.');
// Redirige vers la liste des listes (à adapter selon ta route)
return $this->redirectToRoute('artemis_newsletter_contact_edit',['id'=>$contactList->getId()]);
}
return $this->render('artemis/newsletter/contact/add.twig', [
'form' => $form->createView(),
]);
}
#[Route(path: '/artemis/newsletter/contact/edit',name: 'artemis_newsletter_contact_edit',methods: ['GET', 'POST'])]
public function contactsEdit(ContactRepository $contactRepository): Response
{
}
#[Route(path: '/artemis/newsletter/contact/delete',name: 'artemis_newsletter_contact_delete',methods: ['GET', 'POST'])]
public function contactsDelete(ContactRepository $contactRepository): Response
{
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Controller\Artemis\Newsletter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class TemplateController extends AbstractController
{
#[Route(path: '/artemis/newsletter/template',name: 'artemis_newsletter_template',methods: ['GET', 'POST'])]
public function artemis(): Response
{
}
}

View File

@@ -6,8 +6,11 @@ use App\Repository\CustomerOrderRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
#[ORM\Entity(repositoryClass: CustomerOrderRepository::class)]
#[Vich\Uploadable()]
class CustomerOrder
{
#[ORM\Id]
@@ -36,6 +39,21 @@ class CustomerOrder
#[ORM\OneToMany(targetEntity: CustomerOrderLine::class, mappedBy: 'customerOrder')]
private Collection $customerOrderLines;
#[ORM\Column(length: 255, nullable: true)]
private ?string $state = null;
#[Vich\UploadableField(mapping: 'facture',fileNameProperty: 'factureFileName', size: 'factureSize', mimeType: 'factureMineType', originalName: 'factureOriginalName',dimensions: 'factureDimensions')]
private ?File $facture = null;
#[ORM\Column(nullable: true)]
private ?string $factureFileName = null;
#[ORM\Column(nullable: true)]
private ?array $factureDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $factureSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $factureMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $factureOriginalName = null;
public function __construct()
{
$this->customerOrderLines = new ArrayCollection();
@@ -101,7 +119,7 @@ class CustomerOrder
public function setUpdateAt(?\DateTimeImmutable $updateAt): static
{
$this->updateAt = $updateAt<EFBFBD>;
$this->updateAt = $updateAt;
return $this;
}
@@ -135,4 +153,121 @@ class CustomerOrder
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(?string $state): static
{
$this->state = $state;
return $this;
}
/**
* @param File|null $facture
*/
public function setFacture(?File $facture): void
{
$this->facture = $facture;
if($facture !== null)
$this->updateAt = new \DateTimeImmutable();
}
/**
* @return File|null
*/
public function getFacture(): ?File
{
return $this->facture;
}
/**
* @return array|null
*/
public function getFactureDimensions(): ?array
{
return $this->factureDimensions;
}
/**
* @return string|null
*/
public function getFactureFileName(): ?string
{
return $this->factureFileName;
}
/**
* @param int|null $id
*/
public function setId(?int $id): void
{
$this->id = $id;
}
/**
* @return string|null
*/
public function getFactureMineType(): ?string
{
return $this->factureMineType;
}
/**
* @return string|null
*/
public function getFactureOriginalName(): ?string
{
return $this->factureOriginalName;
}
/**
* @return string|null
*/
public function getFactureSize(): ?string
{
return $this->factureSize;
}
/**
* @param array|null $factureDimensions
*/
public function setFactureDimensions(?array $factureDimensions): void
{
$this->factureDimensions = $factureDimensions;
}
/**
* @param string|null $factureFileName
*/
public function setFactureFileName(?string $factureFileName): void
{
$this->factureFileName = $factureFileName;
}
/**
* @param string|null $factureMineType
*/
public function setFactureMineType(?string $factureMineType): void
{
$this->factureMineType = $factureMineType;
}
/**
* @param string|null $factureOriginalName
*/
public function setFactureOriginalName(?string $factureOriginalName): void
{
$this->factureOriginalName = $factureOriginalName;
}
/**
* @param string|null $factureSize
*/
public function setFactureSize(?string $factureSize): void
{
$this->factureSize = $factureSize;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Entity\Newsletter;
use App\Repository\Newsletter\ContactRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: ContactRepository::class)]
class Contact
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: 'uuid')]
private ?Uuid $uuid = null;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getUuid(): ?Uuid
{
return $this->uuid;
}
public function setUuid(Uuid $uuid): static
{
$this->uuid = $uuid;
return $this;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Form\Artemis\Newsletter;
use App\Entity\Newsletter\Contact;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\NotBlank;
class ContactListType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Nom de la liste',
'constraints' => [
new NotBlank([
'message' => 'Le nom de la liste ne peut pas être vide.',
]),
],
'attr' => [
'placeholder' => 'Ex : Newsletter clients',
],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Contact::class,
]);
}
}

View File

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

View File

@@ -9,6 +9,7 @@ use App\Service\Customer\Billing\CreateDevisCustomerEventSend;
use App\Service\Docuseal\SignClient;
use App\Service\Mailer\Mailer;
use App\Service\Pdf\DevisPdf;
use App\Service\Pdf\FacturePdf;
use App\Service\Pdf\PaymentPdf;
use App\Service\Stancer\Client;
use Doctrine\ORM\EntityManagerInterface;
@@ -26,6 +27,7 @@ use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
#[AsEventListener(event: CreateDevisCustomerEventSend::class, method: 'onBillingEventSend')]
#[AsEventListener(event: CreateAvisEvent::class, method: 'onCreatedAvisEvent')]
#[AsEventListener(event: CreateAvisEventSend::class, method: 'onCreatedAvisEventSend')]
#[AsEventListener(event: CreateFactureEvent::class, method: 'onCreateFactureEvent')]
class BillingEventSusbriber
{
@@ -33,6 +35,15 @@ class BillingEventSusbriber
{
}
public function onCreateFactureEvent(CreateFactureEvent $event)
{
$order = $event->getCustomerOrder();
$isSend = $event->isSend();
$pdf = New FacturePdf($this->kernel,$order);
$pdf->generate();
$pdf->Output('I');
}
public function onCreatedAvisEventSend(CreateAvisEventSend $createAvisEventSend)
{
$createAvis = $createAvisEventSend->getCustomerAdvertPayment();
@@ -78,21 +89,7 @@ class BillingEventSusbriber
foreach ($createAvis->getCustomerAdvertPaymentLines() as $item) {
$total = $total + (floatval($item->getPriceHt()) * 1.20);
}
//$customerStancer = new Customer($customerId);
/*//creeat payement link
$payment = new Payment();
$payment->setAmount($total * 100);
$payment->setCurrency("EUR");
$payment->setDescription("Paiement de l'avis de paiement - " . $createAvis->getNumAvis());
$payment->setCustomer($customerStancer);
$payment->setReturnUrl($paymentReturnPath);
$payment->setOrderId($createAvis->getNumAvis());
$payment->setMethodsAllowed(["card"]);
$payment->setCapture(true);
$paimentId = $data = $payment->send();
$createAvis->setPaymentId($paimentId);
$this->entityManager->persist($createAvis);
$this->entityManager->flush();*/
}
$pdf = New PaymentPdf($this->kernel,$createAvis,$this->urlGenerator->generate('app_payment',[

View File

@@ -0,0 +1,175 @@
<?php
namespace App\Service\Pdf;
use App\Entity\CustomerAdvertPayment;
use App\Entity\CustomerOrder;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\Label\Font\OpenSans;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\Writer\PngWriter;
use Fpdf\Fpdf;
use IntlDateFormatter;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Uid\Uuid;
define('EURO_FACTURE', chr(128));
class FacturePdf extends Fpdf
{
private $items;
public function __construct(private readonly KernelInterface $kernel, private readonly CustomerOrder $customerDevis)
{
parent::__construct();
$items = [];
foreach ($this->customerDevis->getCustomerOrderLines() as $line) {
$items[$line->getPo()] = [
'title' => $line->getName(),
'content' => $line->getContent(),
'priceHt' => $line->getPriceHT(),
'priceTTC' => (1.20 * $line->getPriceHT()),
];
}
ksort($items);
$this->items = $items;
$this->SetTitle(mb_convert_encoding("Facture N° ", "ISO-8859-1", "UTF-8") . $this->customerDevis->getNumOrder());
}
function Header()
{
/*$this->Image($this->kernel->getProjectDir() . "/public/assets/logo_siteconseil.png", 5, 5, 25);
$this->SetFont('Arial', 'B', 12);
$this->Text(30, 10, mb_convert_encoding("SITECONSEIL", 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 12);
$this->Text(30, 15, mb_convert_encoding("27 rue le sérurier", 'ISO-8859-1', 'UTF-8'));
$this->Text(30, 20, mb_convert_encoding("02100 SAINT-QUENTIN", 'ISO-8859-1', 'UTF-8'));
$this->Text(30, 25, mb_convert_encoding("s.com@siteconseil.fr", 'ISO-8859-1', 'UTF-8'));
$this->Text(30, 30, mb_convert_encoding("03 23 05 62 43", 'ISO-8859-1', 'UTF-8'));
$this->Text(8, 35, mb_convert_encoding("SIRET: 41866405800025", 'ISO-8859-1', 'UTF-8'));
$this->Text(8, 40, mb_convert_encoding("RCS: RCS St-Quentin 418 664 058", 'ISO-8859-1', 'UTF-8'));
$this->Text(8, 45, mb_convert_encoding("TVA: FR05418664058", 'ISO-8859-1', 'UTF-8'));*/
$this->SetFont('Arial', '', 10);
$formatter = new IntlDateFormatter(
'fr_FR', // Locale for French (France)
IntlDateFormatter::FULL, // Date style: e.g., jeudi 31 juillet 2025
IntlDateFormatter::NONE, // Time style: none
'Europe/Paris', // Timezone (important for correct date if not UTC)
IntlDateFormatter::GREGORIAN, // Calendar type
);
$this->Text(15, 80, mb_convert_encoding("Facture N° " . $this->customerDevis->getNumOrder(), 'ISO-8859-1', 'UTF-8'));
$this->Text(15, 85, mb_convert_encoding("Saint-Quentin, ".$formatter->format( $this->customerDevis->getCreateAt()->getTimestamp()), 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', 'B', 12);
$y = 60;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getRaisonSocial(), 'ISO-8859-1', 'UTF-8'));
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress(), 'ISO-8859-1', 'UTF-8'));
if ($this->customerDevis->getCustomer()->getAddress2() != "") {
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress2(), 'ISO-8859-1', 'UTF-8'));
}
if ($this->customerDevis->getCustomer()->getAddress3() != "") {
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getAddress3(), 'ISO-8859-1', 'UTF-8'));
}
$y = $y + 5;
$this->Text(110, $y, mb_convert_encoding($this->customerDevis->getCustomer()->getZipcode() . " " . $this->customerDevis->getCustomer()->getCity(), 'ISO-8859-1', 'UTF-8'));
$this->SetFont('Arial', '', 12);
$this->body();
}
public function body()
{
// Headers for the items table
$this->SetFont('Arial','B',10);
$this->SetXY(145,100);
$this->Cell(40, 5, mb_convert_encoding("PRIX HT", "ISO-8859-1", "UTF-8"), 0, 0, 'C');
$this->Line(145, 110, 145, 220);
$this->Line(185, 110, 185, 220);
$this->Line(0,100,5,100);
$this->Line(0,200,5,200);
}
/**
* Generates the main content of the PDF, including the list of items.
* This function has been fixed to correctly display items one after another.
*/
function generate()
{
$this->AliasNbPages();
$this->AddPage();
$this->SetFont('Arial', '', 12);
// Set a starting Y position for the items list, just below the item table header.
$startY = 110;
$this->SetY($startY);
// Define a bottom limit for the content to avoid overwriting the summary/footer.
$contentBottomLimit = 220;
foreach ($this->items as $item) {
// A simple check to see if we have enough space for the next item.
// 30 is a rough estimate for item height. For more complex content,
// you might need a more dynamic height calculation.
if ($this->GetY() + 30 > $contentBottomLimit) {
$this->AddPage();
$this->body(); // Redraw the item table header on the new page
$this->SetY($startY); // Reset Y position on the new page
}
// Store the current Y position to align all columns for this item's title line.
$current_y = $this->GetY();
// Set position to the start of the line for the main content.
$this->SetX(20);
$this->SetFont('Arial', 'B', 11);
// Print the title, but don't move to the next line yet (ln=0).
$this->Cell(95, 10, mb_convert_encoding($item['title'], 'ISO-8859-1', 'UTF-8'), 0, 0);
// Now, set the position for the prices on the same line using SetXY.
$this->SetFont('Arial', '', 11);
$this->SetXY(142, $current_y);
$this->SetFont('Arial', 'B', 11);
$this->Cell(39, 8, number_format($item['priceHt'], 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->SetFont('Arial', '', 11);
// The cursor is now on the line below the title/prices.
// Print the item content description.
$this->SetX(30); // Ensure we are in the correct column.
$this->MultiCell(90, 5, mb_convert_encoding($item['content'], 'ISO-8859-1', 'UTF-8'), 0, 'L');
// Add a small vertical gap between items for readability.
$this->Ln(5);
}
$this->displaySummary();
}
function displaySummary()
{
// Calculate totals
$totalHT = array_sum(array_column($this->items, 'priceHt'));
$totalTVA = $totalHT * 0.20;
$totalTTC = $totalHT + $totalTVA;
// Position the summary block at the bottom of the page
$this->SetY(-60);
// Display the summary
$this->SetFont('Arial', '', 12);
$this->Cell(135, 10, mb_convert_encoding('Total HT :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalHT, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->Cell(135, 10, mb_convert_encoding('TVA (20%) :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalTVA, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
$this->SetFont('Arial', 'B', 12);
$this->Cell(135, 10, mb_convert_encoding('Total :', 'ISO-8859-1', 'UTF-8'), 0, 0, 'R');
$this->Cell(40, 10, number_format($totalTTC, 2, ",") . " " . EURO_FACTURE, 0, 1, 'R');
}
}

View File

@@ -81,22 +81,51 @@
</li>
</ul>
</li>
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="newsletter">
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
<path d="M12 2.252A8.014 8.014 0 0117.748 12H12V2.252z"></path>
</svg>
<span class="ml-3">Newsletter</span>
</div>
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 arrow-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</button>
<ul id="submenu-newsletter" class="submenu ml-6 mt-2 space-y-2">
<li>
<a href="{{ path('artemis_newsletter_contact') }}" class="flex items-center p-2 text-base font-normal text-gray-900 dark:text-white {% if app.request.get('_route') == 'artemis_newsletter_contact' %}bg-gray-200 dark:bg-gray-700{% endif %} rounded-lg">
<span class="ml-3">Liste de contact</span>
</a>
</li>
</ul>
</li>
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="intranet">
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path><path d="M12 2.252A8.014 8.014 0 0117.748 12H12V2.252z"></path></svg>
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
<path d="M12 2.252A8.014 8.014 0 0117.748 12H12V2.252z"></path>
</svg>
<span class="ml-3">Intranet</span>
</div>
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 arrow-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 arrow-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</button>
<ul id="submenu-intranet" class="submenu ml-6 mt-2 space-y-2">
<li>
<a href="{{ path('artemis_intranet_customer') }}" class="flex items-center p-2 text-base font-normal text-gray-900 dark:text-white {% if app.request.get('_route') == "artemis_intranet_customer"%}bg-gray-200 dark:bg-gray-700{% endif%} rounded-lg">
<a href="{{ path('artemis_intranet_customer') }}" class="flex items-center p-2 text-base font-normal text-gray-900 dark:text-white {% if app.request.get('_route') == 'artemis_intranet_customer' %}bg-gray-200 dark:bg-gray-700{% endif %} rounded-lg">
<span class="ml-3">Client(s)</span>
</a>
</li>
</ul>
</li>
{% if is_granted('ROLE_ADMIN') %}
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="settings">

View File

@@ -0,0 +1,37 @@
<div class="mt-2 overflow-x-auto bg-gray-800 rounded-lg shadow">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">N°</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Montant</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Statut</th>
<th class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{% for orderOrder in orderOrders %}
<tr class="hover:bg-gray-700">
<td class="px-6 py-4">FACTURE</td>
<td class="px-6 py-4">{{ orderOrder.numOrder }}</td>
<td class="px-6 py-4">{{ orderOrder.createAt|date('d/m/Y H:i') }}</td>
<td class="px-6 py-4">{{ (orderOrder|totalOrder)|format_currency('EUR') }}</td>
<td class="px-6 py-4 {% if orderOrder.state == "f-send"%} text-green-400 {% else %}text-orange-400{% endif %}">{{ orderOrder.state|trans }}</td>
<td class="px-6 py-4 text-center">
{% if orderOrder.state == "f-created" %}
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',idFacture:orderOrder.id,act:'send'}) }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Envoyée la facture</a>
{% endif %}
{% if orderOrder.state == "f-send" %}
<a href="{{ path('artemis_intranet_customer_view',{id:customer.id,current:'order',idFacture:orderOrder.id,act:'resend'}) }}" class="block bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Réenvoyée la facture</a>
{% endif %}}
<a href="{{ vich_uploader_asset(orderOrder,'facture') }}" download="facture-{{ orderOrder.numOrder }}.pdf" class="block w-full mt-1 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded">Télécharger la facture</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(orderDevis) }}
</div>

View File

@@ -0,0 +1,34 @@
{% extends 'artemis/base.twig' %}
{% block title %}Listes de contacts{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">istes de contacts</h2>
<div>
<a href="{{ path('artemis_newsletter_contact_add') }}" class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
+ Crée une liste de contact
</a>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{% for list in lists %}
<a href="{{ path('artemis_newsletter_contact_edit',{id:list.id}) }}" class="card-contact mt-6">
<div class="card-body flex items-center">
<div class="px-3 py-2 rounded bg-indigo-600 text-white mr-3">
<i class="fad fa-user"></i>
</div>
<div class="flex flex-col">
<h1 class="font-semibold">{{ list.name }}</h1>
<p class="text-xs"><span class="num-2">0</span> Contact</p>
</div>
</div>
</a>
{% else %}
<div class="col-span-4 text-center text-gray-400 dark:text-gray-500 font-bold">Aucune liste trouvée.</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'artemis/base.twig' %}
{% block title %}Créer une nouvelle liste{% endblock %}
{% block content %}
<h1 class="text-2xl font-semibold mb-6">Créer une nouvelle liste de contacts</h1>
<div class="bg-gray-800 rounded-lg shadow p-6 mb-6">
{{ form_start(form, {'attr': {'class': 'max-w-md'}}) }}
<div class="mb-4">
{{ form_label(form.name, null, {'label_attr': {'class': 'block mb-1 font-medium text-gray-700 dark:text-gray-300'}}) }}
{{ form_widget(form.name, {'attr': {
'class': 'w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white'
}}) }}
{{ form_errors(form.name) }}
</div>
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition font-semibold">
Créer la liste
</button>
{{ form_end(form) }}
</div>
{% endblock %}