```
✨ feat(Devis): Ajoute l'entité DevisLine et le formulaire de création de devis non terminer
Ajoute l'entité DevisLine, le formulaire NewDevisType et la route pour la création de devis.
```
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import './admin.scss'
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
|
||||
import TomSelect from "tom-select";
|
||||
// --- INITIALISATION SENTRY (En premier !) ---
|
||||
Sentry.init({
|
||||
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
||||
@@ -22,7 +23,26 @@ Sentry.init({
|
||||
*/
|
||||
function initAdminLayout() {
|
||||
|
||||
|
||||
document.querySelectorAll('select').forEach((el) => {
|
||||
if (!el.tomselect) { // Éviter la double initialisation avec Turbo
|
||||
new TomSelect(el, {
|
||||
controlInput: null,
|
||||
allowEmptyOption: true,
|
||||
highlight: true,
|
||||
plugins: ['dropdown_input'], // Permet d'avoir la recherche dans le dropdown
|
||||
render: {
|
||||
option: function(data, escape) {
|
||||
return `<div class="py-2 px-3">
|
||||
<div class="text-[13px] font-bold text-white">${escape(data.text)}</div>
|
||||
</div>`;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
return `<div class="text-blue-400 font-bold">${escape(data.text)}</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
const imageInput = document.getElementById('product_image_input');
|
||||
const previewImage = document.getElementById('product-image-preview');
|
||||
const placeholderIcon = document.getElementById('product-image-placeholder');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
@import "tom-select/dist/css/tom-select.css";
|
||||
|
||||
|
||||
form {
|
||||
label {
|
||||
color: white !important;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn { animation: fadeIn 0.3s ease-in-out; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
@@ -23,3 +23,333 @@ form {
|
||||
/* Menu Accordion sans JS */
|
||||
details summary::-webkit-details-marker { display:none; }
|
||||
details[open] .arrow-icon { transform: rotate(180deg); }
|
||||
|
||||
.ts-control {
|
||||
border: 1px solid rgba(255, 255, 255, 0.05); /* Dark border */
|
||||
padding: 8px 8px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none; /* Removed inset shadow */
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: rgba(15, 23, 42, 0.6) !important; /* Slate-900 transparent */
|
||||
}
|
||||
.ts-wrapper.multi.has-items .ts-control {
|
||||
padding: calc(8px - 2px - 1px) 8px calc(8px - 2px - 3px - 1px);
|
||||
}
|
||||
.full .ts-control {
|
||||
background-color: #0f172a; /* Slate-900 */
|
||||
}
|
||||
.disabled .ts-control, .disabled .ts-control * {
|
||||
cursor: default !important;
|
||||
}
|
||||
.focus .ts-control {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); /* Blue halo */
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.ts-control > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
.ts-wrapper.multi .ts-control > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 2px 6px;
|
||||
background: #2563eb; /* Blue-600 */
|
||||
color: #fff;
|
||||
border: 1px solid #1d4ed8;
|
||||
}
|
||||
.ts-wrapper.multi .ts-control > div.active {
|
||||
background: #1e40af; /* Deeper blue */
|
||||
color: #fff;
|
||||
border: 1px solid #1e3a8a;
|
||||
}
|
||||
.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {
|
||||
color: #475569;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
.ts-control > input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 7rem;
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
-webkit-user-select: auto !important;
|
||||
-moz-user-select: auto !important;
|
||||
-ms-user-select: auto !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
color: #f8fafc !important; /* Slate-50 text */
|
||||
}
|
||||
.ts-control > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
.ts-control > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.has-items .ts-control > input {
|
||||
margin: 0 4px !important;
|
||||
}
|
||||
.ts-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
.ts-control.rtl.single .ts-control:after {
|
||||
left: 15px;
|
||||
right: auto;
|
||||
}
|
||||
.ts-control.rtl .ts-control > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
.disabled .ts-control {
|
||||
opacity: 0.5;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
.input-hidden .ts-control > input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
|
||||
.ts-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: #0f172a; /* Dark background */
|
||||
margin: 0.25rem 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0 0 3px 3px;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.ts-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ts-dropdown [data-selectable] .highlight {
|
||||
background: rgba(59, 130, 246, 0.3); /* Blue highlight */
|
||||
border-radius: 1px;
|
||||
}
|
||||
.ts-dropdown .option,
|
||||
.ts-dropdown .optgroup-header,
|
||||
.ts-dropdown .no-results,
|
||||
.ts-dropdown .create {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ts-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ts-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
.ts-dropdown .optgroup-header {
|
||||
color: #94a3b8; /* Slate-400 */
|
||||
background: #0f172a;
|
||||
cursor: default;
|
||||
}
|
||||
.ts-dropdown .active {
|
||||
background-color: #1e293b; /* Slate-800 */
|
||||
color: #3b82f6; /* Blue-500 */
|
||||
}
|
||||
.ts-dropdown .active.create {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.ts-dropdown .create {
|
||||
color: rgba(248, 248, 248, 0.5);
|
||||
}
|
||||
.ts-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 5px 8px;
|
||||
}
|
||||
.ts-dropdown .spinner::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #1e293b;
|
||||
border-color: #3b82f6 transparent #3b82f6 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ts-dropdown-content {
|
||||
overflow: hidden auto;
|
||||
max-height: 200px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-drag_drop .ts-dragging {
|
||||
color: transparent !important;
|
||||
}
|
||||
.ts-wrapper.plugin-drag_drop .ts-dragging > * {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.plugin-checkbox_options:not(.rtl) .option input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.plugin-checkbox_options.rtl .option input {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* stylelint-disable function-name-case */
|
||||
.plugin-clear_button {
|
||||
--ts-pr-clear-button: 1em;
|
||||
}
|
||||
.plugin-clear_button .clear-button {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: calc(8px - 6px);
|
||||
margin-right: 0 !important;
|
||||
background: transparent !important;
|
||||
transition: opacity 0.5s;
|
||||
cursor: pointer;
|
||||
color: #ef4444; /* Red for clear */
|
||||
}
|
||||
.plugin-clear_button.focus.has-items .clear-button, .plugin-clear_button:not(.disabled):hover.has-items .clear-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ts-wrapper .dropdown-header {
|
||||
position: relative;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: #1e293b;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.ts-wrapper .dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
color: #f8fafc;
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.ts-wrapper .dropdown-header-close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.plugin-dropdown_input.focus.dropdown-active .ts-control {
|
||||
box-shadow: none;
|
||||
border: 1px solid #3b82f6;
|
||||
}
|
||||
.plugin-dropdown_input .dropdown-input {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-width: 0 0 1px;
|
||||
display: block;
|
||||
padding: 8px 8px;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ts-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ts-wrapper.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 0 6px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ts-wrapper.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ts-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ts-dropdown,
|
||||
.ts-control,
|
||||
.ts-control input {
|
||||
color: #f8fafc; /* Global text color */
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ts-control,
|
||||
.ts-wrapper.single.input-active .ts-control {
|
||||
background: rgba(15, 23, 42, 0.6) !important;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.ts-wrapper.single .ts-control::after {
|
||||
border-color: #64748b transparent transparent transparent; /* Slate-500 arrow */
|
||||
}
|
||||
|
||||
.ts-wrapper.single.dropdown-active .ts-control::after {
|
||||
border-color: transparent transparent #3b82f6 transparent;
|
||||
}
|
||||
|
||||
.ts-wrapper.multi .ts-control [data-value] {
|
||||
text-shadow: none;
|
||||
border-radius: 3px;
|
||||
background-color: #3b82f6;
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ts-wrapper.multi.disabled .ts-control [data-value] {
|
||||
color: #475569;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.ts-wrapper.single .ts-control {
|
||||
box-shadow: none;
|
||||
background-color: rgba(15, 23, 42, 0.6) !important;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.ts-wrapper.single .ts-control, .ts-dropdown.single {
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.ts-dropdown .optgroup {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
0
assets/tom.scss
Normal file
0
assets/tom.scss
Normal file
35
migrations/Version20260116143611.php
Normal file
35
migrations/Version20260116143611.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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 Version20260116143611 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 devis_line (id SERIAL NOT NULL, devi_id INT DEFAULT NULL, pos INT NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, price_ht DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_9EC6D529131098A5 ON devis_line (devi_id)');
|
||||
$this->addSql('ALTER TABLE devis_line ADD CONSTRAINT FK_9EC6D529131098A5 FOREIGN KEY (devi_id) REFERENCES devis (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
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 devis_line DROP CONSTRAINT FK_9EC6D529131098A5');
|
||||
$this->addSql('DROP TABLE devis_line');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use App\Entity\Devis;
|
||||
use App\Form\NewDevisType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\AccountRepository;
|
||||
use App\Repository\DevisRepository;
|
||||
@@ -26,9 +28,21 @@ class DevisController extends AbstractController
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/crm/devis/add', name: 'app_crm_devis_add', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function devisAdd(AccountRepository $accountRepository, AppLogger $appLogger): Response
|
||||
public function devisAdd(DevisRepository $devisRepository, AppLogger $appLogger): Response
|
||||
{
|
||||
$devisNumber ="DEVIS-".sprintf('%05d',$devisRepository->count()+1);
|
||||
$appLogger->record('VIEW', 'Consultation de la création d\'un devis');
|
||||
|
||||
$devis = new Devis();
|
||||
$devis->setNum($devisNumber);
|
||||
$devis->setState("draft");
|
||||
$devis->setCreateA(new \DateTimeImmutable());
|
||||
$devis->setUpdateAt(new \DateTimeImmutable());
|
||||
|
||||
$form = $this->createForm(NewDevisType::class,$devis);
|
||||
return $this->render('dashboard/devis/add.twig',[
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\DevisRepository;
|
||||
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\Attribute\Uploadable;
|
||||
@@ -63,6 +65,17 @@ class Devis
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $signatureId = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, DevisLine>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DevisLine::class, mappedBy: 'devi')]
|
||||
private Collection $devisLines;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->devisLines = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -352,4 +365,34 @@ class Devis
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, DevisLine>
|
||||
*/
|
||||
public function getDevisLines(): Collection
|
||||
{
|
||||
return $this->devisLines;
|
||||
}
|
||||
|
||||
public function addDevisLine(DevisLine $devisLine): static
|
||||
{
|
||||
if (!$this->devisLines->contains($devisLine)) {
|
||||
$this->devisLines->add($devisLine);
|
||||
$devisLine->setDevi($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDevisLine(DevisLine $devisLine): static
|
||||
{
|
||||
if ($this->devisLines->removeElement($devisLine)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($devisLine->getDevi() === $this) {
|
||||
$devisLine->setDevi(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
96
src/Entity/DevisLine.php
Normal file
96
src/Entity/DevisLine.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\DevisLineRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DevisLineRepository::class)]
|
||||
class DevisLine
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'devisLines')]
|
||||
private ?Devis $devi = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $pos = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private ?string $content = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $priceHt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getDevi(): ?Devis
|
||||
{
|
||||
return $this->devi;
|
||||
}
|
||||
|
||||
public function setDevi(?Devis $devi): static
|
||||
{
|
||||
$this->devi = $devi;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPos(): ?int
|
||||
{
|
||||
return $this->pos;
|
||||
}
|
||||
|
||||
public function setPos(int $pos): static
|
||||
{
|
||||
$this->pos = $pos;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): static
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPriceHt(): ?float
|
||||
{
|
||||
return $this->priceHt;
|
||||
}
|
||||
|
||||
public function setPriceHt(float $priceHt): static
|
||||
{
|
||||
$this->priceHt = $priceHt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
56
src/Form/NewDevisType.php
Normal file
56
src/Form/NewDevisType.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Devis;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NewDevisType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('num', TextType::class, [
|
||||
'label' => 'Numéro du devis',
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
'readonly' => true,
|
||||
]
|
||||
])
|
||||
->add('createA', DateType::class, [
|
||||
'label' => 'Date du devis',
|
||||
'required' => true,
|
||||
'widget' => 'single_text', // Recommandé pour un meilleur rendu HTML5
|
||||
'attr' => [
|
||||
'readonly' => true,
|
||||
]
|
||||
])
|
||||
->add('customer', EntityType::class, [
|
||||
'label' => 'Client',
|
||||
'required' => true,
|
||||
'class' => Customer::class,
|
||||
// Utilisation d'une fonction anonyme pour concaténer Nom et Prénom
|
||||
'choice_label' => function (Customer $customer) {
|
||||
return sprintf('%s - %s',
|
||||
strtoupper($customer->getSurname()), // Nom en majuscules
|
||||
$customer->getName() // Prénom
|
||||
);
|
||||
},
|
||||
'placeholder' => 'Sélectionnez un client...',
|
||||
'attr' => [
|
||||
'class' => 'select2' // Optionnel : si tu utilises Select2 ou TomSelect
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefault('data_class', Devis::class);
|
||||
}
|
||||
}
|
||||
43
src/Repository/DevisLineRepository.php
Normal file
43
src/Repository/DevisLineRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\DevisLine;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<DevisLine>
|
||||
*/
|
||||
class DevisLineRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, DevisLine::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return DevisLine[] Returns an array of DevisLine objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('d')
|
||||
// ->andWhere('d.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('d.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?DevisLine
|
||||
// {
|
||||
// return $this->createQueryBuilder('d')
|
||||
// ->andWhere('d.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
107
templates/dashboard/devis/add.twig
Normal file
107
templates/dashboard/devis/add.twig
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends 'dashboard/base.twig' %}
|
||||
|
||||
{% block title %}Création Devis{% endblock %}
|
||||
{% block title_header %}Nouveau <span class="text-blue-500">Devis</span>{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
<a href="{{ path('app_crm_devis') }}" class="flex items-center px-4 py-2 text-[10px] font-black text-slate-400 hover:text-white uppercase tracking-widest transition-all group">
|
||||
<svg class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Annuler
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="w-full animate-in fade-in zoom-in-95 duration-500">
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[3rem] p-10 shadow-2xl relative overflow-hidden w-full">
|
||||
|
||||
{# Décoration de fond #}
|
||||
<div class="absolute top-0 right-0 -mr-16 -mt-16 w-96 h-96 bg-blue-600/5 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
{# Header #}
|
||||
<div class="relative mb-12">
|
||||
<div class="flex items-center space-x-6">
|
||||
<div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-blue-600 shadow-lg shadow-blue-600/30 text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-white tracking-tight">Création d'un devis</h3>
|
||||
<p class="text-[9px] text-slate-500 uppercase tracking-[0.3em] font-bold mt-1">Configuration des informations d'entête</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative space-y-10">
|
||||
{# GRILLE À 3 COLONNES #}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{# COLONNE 1 : NUMÉRO #}
|
||||
<div class="space-y-3">
|
||||
{{ form_label(form.num) }}
|
||||
<div class="relative">
|
||||
{{ form_widget(form.num) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# COLONNE 2 : DATE #}
|
||||
<div class="space-y-3">
|
||||
{{ form_label(form.createA) }}
|
||||
<div class="relative">
|
||||
{{ form_widget(form.createA) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# COLONNE 3 : CLIENT #}
|
||||
<div class="space-y-3">
|
||||
{{ form_label(form.customer) }}
|
||||
<div class="relative">
|
||||
{{ form_widget(form.customer) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# BOUTON LARGEUR TOTALE #}
|
||||
<div class="pt-4">
|
||||
<button type="submit" class="w-full py-6 bg-blue-600 hover:bg-blue-500 text-white text-[11px] font-black uppercase tracking-[0.5em] rounded-2xl shadow-2xl shadow-blue-600/40 transition-all hover:scale-[1.005] active:scale-95 flex items-center justify-center group">
|
||||
<span>Valider et créer le devis</span>
|
||||
<svg class="w-5 h-5 ml-4 transform group-hover:translate-x-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7m0 0l-7 7m7-7H3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
label {
|
||||
@apply block text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3 ml-2 !important;
|
||||
}
|
||||
|
||||
input, select {
|
||||
@apply w-full bg-slate-900/60 border border-white/5 rounded-2xl px-6 py-5 text-sm text-white outline-none focus:border-blue-500/50 focus:bg-slate-900/80 transition-all duration-500 !important;
|
||||
}
|
||||
|
||||
input[readonly] {
|
||||
@apply border-white/5 bg-white/5 cursor-not-allowed text-slate-500 !important;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply appearance-none cursor-pointer;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%233b82f6'%3E%3Cpath stroke-linecap='round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"%3E%3C/path%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1.5rem center;
|
||||
background-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user