first commit
This commit is contained in:
@@ -3,6 +3,8 @@ import './admin.scss'
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import TomSelect from "tom-select";
|
||||
import {RepeatLine} from "./libs/RepeatLine.js";
|
||||
import {DevisManager} from "./libs/DevisManager.js";
|
||||
// --- INITIALISATION SENTRY (En premier !) ---
|
||||
Sentry.init({
|
||||
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
||||
@@ -22,7 +24,10 @@ Sentry.init({
|
||||
* Initialise les composants de l'interface d'administration.
|
||||
*/
|
||||
function initAdminLayout() {
|
||||
|
||||
if (!customElements.get('copy-text')) {
|
||||
customElements.define('repeat-line', RepeatLine, {extends: 'div'})
|
||||
customElements.define('devis-manager', DevisManager, {extends: 'div'})
|
||||
}
|
||||
document.querySelectorAll('select').forEach((el) => {
|
||||
if (!el.tomselect) { // Éviter la double initialisation avec Turbo
|
||||
new TomSelect(el, {
|
||||
|
||||
46
assets/libs/DevisManager.js
Normal file
46
assets/libs/DevisManager.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// SUPPRIME l'import de postcss ici
|
||||
|
||||
export class DevisManager extends HTMLDivElement {
|
||||
connectedCallback() {
|
||||
this.customerSelect = this.querySelector('select');
|
||||
// On remonte au parent pour trouver les selects d'adresse hors du bloc customer
|
||||
this.billAddress = this.parentElement.querySelector('#billAddress');
|
||||
this.shipAddress = this.parentElement.querySelector('#shipAddress');
|
||||
|
||||
if (this.customerSelect) {
|
||||
this.customerSelect.addEventListener('change', (e) => this.updateCustomerInfo(e.target.value));
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomerInfo(customerId) {
|
||||
if (!customerId) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch("/crm/customer/address/" + customerId);
|
||||
const data = await resp.json();
|
||||
|
||||
// Vider les anciens selects
|
||||
data.addressList.forEach(itemList => {
|
||||
this.billAddress.tomselect.addOption({
|
||||
value: itemList.id,
|
||||
text :itemList.label
|
||||
})
|
||||
this.shipAddress.tomselect.addOption({
|
||||
value: itemList.id,
|
||||
text :itemList.label
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des adresses:", error);
|
||||
}
|
||||
}
|
||||
|
||||
createOption(itemList) {
|
||||
// Pas besoin d'import, 'document' est global dans le navigateur
|
||||
const option = document.createElement('option');
|
||||
option.value = itemList.id;
|
||||
option.textContent = itemList.label;
|
||||
return option;
|
||||
}
|
||||
|
||||
}
|
||||
119
assets/libs/RepeatLine.js
Normal file
119
assets/libs/RepeatLine.js
Normal file
@@ -0,0 +1,119 @@
|
||||
export class RepeatLine extends HTMLDivElement{
|
||||
connectedCallback(){
|
||||
this.$props = this.getProps(this, { maxRows: 20 });
|
||||
this.$refs = this.getRefs(this);
|
||||
|
||||
this.rowHTML = this.$refs.rows.children[0].outerHTML;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Hook up events for the row.
|
||||
setUpRow(row) {
|
||||
const rowRefs = this.getRefs(row);
|
||||
|
||||
rowRefs.removeButton.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
this.removeRow(row);
|
||||
};
|
||||
}
|
||||
|
||||
// Enable or disable addButton as necessary.
|
||||
updateAddButton() {
|
||||
if (this.$refs.rows.children.length >= this.$props.maxRows) {
|
||||
this.$refs.addButton.setAttribute('disabled', '');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs.addButton.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
// Update array key values to the row number
|
||||
updateFieldNames() {
|
||||
[...this.$refs.rows.children]
|
||||
.forEach((el, index) => {
|
||||
el.querySelectorAll('[name]')
|
||||
.forEach(el => {
|
||||
const newName = el.getAttribute('name').replace(/\[\d\]/gm, `[${index}]`);
|
||||
el.setAttribute('name', newName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addRow() {
|
||||
if (
|
||||
!this.rowHTML ||
|
||||
this.$refs.rows.children.length >= this.$props.maxRows
|
||||
) return;
|
||||
|
||||
let newRow = this.createFromHTML(this.rowHTML);
|
||||
newRow.removeAttribute('id');
|
||||
this.setUpRow(newRow);
|
||||
|
||||
this.$refs.rows.appendChild(newRow);
|
||||
newRow.querySelector('input,textarea,select').focus();
|
||||
newRow.querySelectorAll('input,textarea,select').forEach(el=>{
|
||||
el.setAttribute('value','');
|
||||
el.value = "";
|
||||
})
|
||||
this.updateFieldNames();
|
||||
this.updateAddButton();
|
||||
}
|
||||
|
||||
removeRow(row) {
|
||||
if (this.$refs.rows.children.length <= 1) return;
|
||||
|
||||
row.remove();
|
||||
this.$refs.rows.focus();
|
||||
|
||||
this.updateFieldNames();
|
||||
this.updateFieldNames();
|
||||
}
|
||||
init() {
|
||||
this.setUpRow(this.$refs.rows.children[0]);
|
||||
|
||||
this.$refs.addButton.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
this.addRow();
|
||||
}
|
||||
|
||||
this.updateFieldNames();
|
||||
}
|
||||
|
||||
// Return an object that contains references to DOM objects.
|
||||
getRefs(el) {
|
||||
let result = {};
|
||||
|
||||
[...el.querySelectorAll('[data-ref]')]
|
||||
.forEach(ref => {
|
||||
result[ref.dataset.ref] = ref;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
setDefaults(obj, defaults) {
|
||||
let results = obj;
|
||||
|
||||
for (const prop in defaults) {
|
||||
if (!obj.hasOwnProperty(prop)) {
|
||||
results[prop] = defaults[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getProps(el, defaults={}) {
|
||||
return this.setDefaults(
|
||||
JSON.parse(el.dataset.props ?? '{}'),
|
||||
defaults
|
||||
);
|
||||
}
|
||||
|
||||
createFromHTML(html='') {
|
||||
let element = document.createElement(null);
|
||||
element.innerHTML = html;
|
||||
return element.firstElementChild;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
"docusealco/docuseal-php": "^1.0.5",
|
||||
"endroid/qr-code": "^6.0.9",
|
||||
"exbil/mailcow-php-api": ">=0.15.0",
|
||||
"fpdf/fpdf": ">=1.86.1",
|
||||
"fpdf/fpdf": "^1.86",
|
||||
"google/apiclient": "^2.19.0",
|
||||
"google/cloud": "^0.296.0",
|
||||
"healey/robots": "^1.0.1",
|
||||
|
||||
2
composer.lock
generated
2
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "42643761eadf7db812cdd14bca4e4106",
|
||||
"content-hash": "4d8f04690344698e5031e5b37786c9f9",
|
||||
"packages": [
|
||||
{
|
||||
"name": "async-aws/core",
|
||||
|
||||
@@ -335,7 +335,7 @@ class AccountController extends AbstractController
|
||||
|
||||
// --- SÉCURITÉ : Interdiction suppression ROOT ou CLIENT_MAIN ---
|
||||
// On vérifie si l'un des rôles protégés est présent dans le tableau des rôles du compte
|
||||
$protectedRoles = ['ROLE_ROOT', 'ROLE_CLIENT_MAIN'];
|
||||
$protectedRoles = ['ROLE_ROOT'];
|
||||
$accountRoles = $account->getRoles();
|
||||
|
||||
// Si l'intersection entre les rôles du compte et les rôles protégés n'est pas vide
|
||||
|
||||
@@ -25,6 +25,21 @@ class CustomerController extends AbstractController
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
) {}
|
||||
|
||||
#[Route(path: '/crm/customer/address/{id}', name: 'app_crm_customer_address', methods: ['GET'])]
|
||||
public function customerAddress(?Customer $customer)
|
||||
{
|
||||
$addressList = [];
|
||||
foreach ($customer->getCustomerAddresses() as $address) {
|
||||
$addressList[] = [
|
||||
'id' => $address->getId(),
|
||||
'label' => $address->getAddress()." ".$address->getZipcode()." ".$address->getCity(),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->json([
|
||||
'addressList' => $addressList,
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/crm/customer', name: 'app_crm_customer', methods: ['GET'])]
|
||||
public function index(
|
||||
PaginatorInterface $paginator,
|
||||
|
||||
@@ -3,15 +3,20 @@
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use App\Entity\Devis;
|
||||
use App\Entity\DevisLine;
|
||||
use App\Form\NewDevisType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\AccountRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\DevisRepository;
|
||||
use App\Service\Pdf\DevisPdfService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Knp\Bundle\PaginatorBundle\KnpPaginatorBundle;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class DevisController extends AbstractController
|
||||
@@ -20,15 +25,20 @@ class DevisController extends AbstractController
|
||||
* Liste des administrateurs
|
||||
*/
|
||||
#[Route(path: '/crm/devis', name: 'app_crm_devis', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function devis(DevisRepository $devisRepository,AppLogger $appLogger,PaginatorInterface $paginator,Request $request): Response
|
||||
public function devis(KernelInterface $kernel,DevisRepository $devisRepository,AppLogger $appLogger,PaginatorInterface $paginator,Request $request): Response
|
||||
{
|
||||
$devis = $devisRepository->findAll()[0];
|
||||
$df = new DevisPdfService($kernel,$devis);
|
||||
$df->generate();
|
||||
|
||||
$df->Output('I');
|
||||
$appLogger->record('VIEW', 'Consultation de la liste des devis');
|
||||
return $this->render('dashboard/devis/list.twig',[
|
||||
'quotes' => $paginator->paginate($devisRepository->findBy([],['createA'=>'asc']),$request->get('page', 1),20),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/crm/devis/add', name: 'app_crm_devis_add', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function devisAdd(DevisRepository $devisRepository, AppLogger $appLogger): Response
|
||||
#[Route(path: '/crm/devis/add', name: 'app_crm_devis_add', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function devisAdd(EntityManagerInterface $entityManager,CustomerRepository $customerRepository,DevisRepository $devisRepository, AppLogger $appLogger,Request $request): Response
|
||||
{
|
||||
$devisNumber ="DEVIS-".sprintf('%05d',$devisRepository->count()+1);
|
||||
$appLogger->record('VIEW', 'Consultation de la création d\'un devis');
|
||||
@@ -40,6 +50,21 @@ class DevisController extends AbstractController
|
||||
$devis->setUpdateAt(new \DateTimeImmutable());
|
||||
|
||||
$form = $this->createForm(NewDevisType::class,$devis);
|
||||
if($request->isMethod('POST')){
|
||||
$devis->setCustomer($customerRepository->find($_POST['new_devis']['customer']));
|
||||
foreach ($_POST['lines'] as $cd=>$line) {
|
||||
$rLine = new DevisLine();
|
||||
$rLine->setDevi($devis);
|
||||
$rLine->setPos($cd);
|
||||
$rLine->setTitle($line['title']);
|
||||
$rLine->setContent($line['description']);
|
||||
$rLine->setPriceHt(floatval($line['price']));
|
||||
$entityManager->persist($rLine);
|
||||
}
|
||||
$entityManager->persist($devis);
|
||||
$entityManager->flush();
|
||||
|
||||
}
|
||||
return $this->render('dashboard/devis/add.twig',[
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
|
||||
160
src/Service/Pdf/DevisPdfService.php
Normal file
160
src/Service/Pdf/DevisPdfService.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Pdf;
|
||||
|
||||
use App\Entity\Devis;
|
||||
use Fpdf\Fpdf;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class DevisPdfService extends Fpdf
|
||||
{
|
||||
private Devis $devis;
|
||||
private string $logo;
|
||||
|
||||
public function __construct(KernelInterface $kernel, Devis $devis, $orientation = 'P', $unit = 'mm', $size = 'A4')
|
||||
{
|
||||
parent::__construct($orientation, $unit, $size);
|
||||
$this->devis = $devis;
|
||||
$this->logo = $kernel->getProjectDir()."/public/provider/images/favicon.png";
|
||||
|
||||
$this->AliasNbPages();
|
||||
$this->SetAutoPageBreak(true, 35);
|
||||
}
|
||||
|
||||
private function clean(?string $text): string
|
||||
{
|
||||
if (!$text) return '';
|
||||
|
||||
// On s'assure que l'entrée est bien vue comme de l'UTF-8
|
||||
// //TRANSLIT : tente de remplacer par un caractère proche (ex: é -> e)
|
||||
// //IGNORE : supprime purement et simplement les caractères impossibles à convertir
|
||||
return iconv('UTF-8', 'windows-1252//TRANSLIT//IGNORE', $text);
|
||||
}
|
||||
|
||||
public function Header()
|
||||
{
|
||||
$this->SetY(10);
|
||||
if (file_exists($this->logo)) {
|
||||
$this->Image($this->logo, 10, 10, 12);
|
||||
$this->SetX(25);
|
||||
}
|
||||
|
||||
$this->SetFont('Arial', 'B', 14);
|
||||
$this->Cell(0, 7, $this->clean('LUDIKEVENT'), 0, 1, 'L');
|
||||
|
||||
$this->SetX(25);
|
||||
$this->SetFont('Arial', '', 8);
|
||||
$this->SetTextColor(80, 80, 80);
|
||||
$this->Cell(0, 4, $this->clean('SIRET : 930 488 408 00012 | RCS : 930 488 408'), 0, 1, 'L');
|
||||
|
||||
$this->SetX(25);
|
||||
$this->Cell(0, 4, $this->clean('Tél. : 06 14 17 24 47'), 0, 1, 'L');
|
||||
|
||||
$this->SetX(25);
|
||||
$this->SetTextColor(37, 99, 235);
|
||||
$this->Cell(0, 4, $this->clean('contact@ludikevent.fr | www.ludikevent.fr'), 0, 1, 'L', false, 'https://www.ludikevent.fr');
|
||||
|
||||
$this->SetY(40);
|
||||
$this->SetFont('Arial', 'B', 16);
|
||||
$this->SetTextColor(37, 99, 235);
|
||||
$this->Cell(0, 10, $this->clean('DEVIS N° ' . $this->devis->getNum()), 0, 1, 'L');
|
||||
|
||||
$this->SetDrawColor(37, 99, 235);
|
||||
$this->SetLineWidth(0.5);
|
||||
$this->Line(10, $this->GetY(), 200, $this->GetY());
|
||||
$this->Ln(10);
|
||||
}
|
||||
|
||||
public function generate(): string
|
||||
{
|
||||
$this->AddPage();
|
||||
$customer = $this->devis->getCustomer();
|
||||
|
||||
// BLOC CLIENT À DROITE
|
||||
$this->SetY(55);
|
||||
$this->SetFont('Arial', 'B', 9);
|
||||
$this->SetTextColor(100, 100, 100);
|
||||
$this->Cell(0, 5, $this->clean('DESTINATAIRE'), 0, 1, 'R');
|
||||
|
||||
$this->SetTextColor(0, 0, 0);
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Cell(0, 7, $this->clean($customer->getName()), 0, 1, 'R');
|
||||
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$this->SetTextColor(50, 50, 50);
|
||||
|
||||
$surname = method_exists($customer, 'getSurname') ? $customer->getSurname() : '';
|
||||
if ($surname) {
|
||||
$this->Cell(0, 5, $this->clean($customer->getName() . ' ' . $surname), 0, 1, 'R');
|
||||
}
|
||||
|
||||
if ($customer->getPhone()) {
|
||||
$this->Cell(0, 5, $this->clean('Tél : ' . $customer->getPhone()), 0, 1, 'R');
|
||||
}
|
||||
|
||||
if ($customer->getEmail()) {
|
||||
$this->SetTextColor(37, 99, 235);
|
||||
$this->Cell(0, 5, $this->clean($customer->getEmail()), 0, 1, 'R');
|
||||
}
|
||||
|
||||
$this->Ln(15);
|
||||
|
||||
// --- TABLEAU (Sans colonne Quantité) ---
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->SetFillColor(245, 247, 250);
|
||||
$this->SetDrawColor(200, 200, 200);
|
||||
$this->SetTextColor(0, 0, 0);
|
||||
|
||||
// Largeurs : Désignation (150) + Total HT (40) = 190mm
|
||||
$this->Cell(150, 10, $this->clean('Désignation'), 1, 0, 'L', true);
|
||||
$this->Cell(40, 10, $this->clean('Total HT'), 1, 1, 'R', true);
|
||||
|
||||
$this->SetFont('Arial', '', 10);
|
||||
$totalHT = 0;
|
||||
|
||||
foreach ($this->devis->getDevisLines() as $line) {
|
||||
$ht = $line->getPriceHt();
|
||||
$totalHT += $ht;
|
||||
|
||||
$currentY = $this->GetY();
|
||||
// MultiCell pour la description longue
|
||||
$this->MultiCell(150, 8, $this->clean($line->getTitle()), 1, 'L');
|
||||
$endY = $this->GetY();
|
||||
$h = $endY - $currentY;
|
||||
|
||||
// On se replace à droite de la MultiCell pour le prix
|
||||
$this->SetXY(160, $currentY);
|
||||
$this->Cell(40, $h, number_format($ht, 2, ',', ' ') . $this->clean(' €'), 1, 1, 'R');
|
||||
}
|
||||
|
||||
// --- TOTAUX ---
|
||||
$this->Ln(5);
|
||||
$this->SetFont('Arial', 'B', 10);
|
||||
$this->Cell(120);
|
||||
$this->Cell(30, 8, $this->clean('TOTAL HT'), 0, 0, 'L');
|
||||
$this->Cell(40, 8, number_format($totalHT, 2, ',', ' ') . $this->clean(' €'), 0, 1, 'R');
|
||||
|
||||
$this->Cell(120);
|
||||
$this->SetTextColor(37, 99, 235);
|
||||
$this->SetFont('Arial', 'B', 12);
|
||||
$this->Cell(30, 10, $this->clean('TOTAL TTC'), 0, 0, 'L');
|
||||
$this->Cell(40, 10, number_format($totalHT * 1.20, 2, ',', ' ') . $this->clean(' €'), 0, 1, 'R');
|
||||
|
||||
return $this->Output('S');
|
||||
}
|
||||
|
||||
public function Footer()
|
||||
{
|
||||
$this->SetY(-15);
|
||||
$this->SetFont('Arial', 'I', 8);
|
||||
$this->SetTextColor(128, 128, 128);
|
||||
|
||||
$num = 'Devis N' . chr(176) . ' ' . $this->devis->getNum();
|
||||
$date = 'Date : ' . ($this->devis->getCreateA() ? $this->devis->getCreateA()->format('d/m/Y') : date('d/m/Y'));
|
||||
$page = 'Page ' . $this->PageNo() . '/{nb}';
|
||||
|
||||
$this->Cell(63, 10, $this->clean($num), 0, 0, 'L');
|
||||
$this->Cell(63, 10, $this->clean($date), 0, 0, 'C');
|
||||
$this->Cell(64, 10, $this->clean($page), 0, 0, 'R');
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@
|
||||
</a>
|
||||
|
||||
{# Bouton Supprimer avec protection ROOT/CLIENT_MAIN #}
|
||||
{% if 'ROLE_ROOT' not in admin.roles and 'ROLE_CLIENT_MAIN' not in admin.roles %}
|
||||
{% if 'ROLE_ROOT' not in admin.roles %}
|
||||
<a href="{{ path('app_crm_administrateur_delete', {id: admin.id}) }}?_token={{ csrf_token('delete' ~ admin.id) }}"
|
||||
data-turbo-method="post"
|
||||
data-turbo-confirm="Confirmer la suppression définitive de l'Administrateur {{ admin.firstName }} {{ admin.name }} ?"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% block body %}
|
||||
<div class="w-full animate-in fade-in zoom-in-95 duration-500">
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_start(form,{attr:{'data-turbo':'false'}}) }}
|
||||
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[3rem] p-10 shadow-2xl relative overflow-hidden w-full">
|
||||
|
||||
@@ -58,15 +58,69 @@
|
||||
</div>
|
||||
|
||||
{# COLONNE 3 : CLIENT #}
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3" is="devis-manager">
|
||||
{{ form_label(form.customer) }}
|
||||
<div class="relative">
|
||||
{{ form_widget(form.customer) }}
|
||||
</div>
|
||||
{{ form_widget(form.customer) }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="billAddress">Adresse de facturation</label>
|
||||
<select id="billAddress" name="devis[ship_address]">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="shipAddress">Adresse de livraison</label>
|
||||
<select id="shipAddress" name="devis[ship_address]">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-repeater" data-component="repeater" is="repeat-line">
|
||||
<ol class="form-repeater__rows" data-ref="rows" tabindex="0">
|
||||
<li class="form-repeater__row" style="border-bottom: 1px solid white">
|
||||
<fieldset class="form-group form-group--horizontal">
|
||||
<div class="flex space-x-4">
|
||||
<div class="flex-1">
|
||||
<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="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 HT</label>
|
||||
<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"
|
||||
data-ref="removeButton"
|
||||
type="button"
|
||||
>
|
||||
Supprimer la ligne
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<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="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>
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
</li>
|
||||
</ol>
|
||||
<button
|
||||
class="mt-2 form-repeater__add-button w-full bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded"
|
||||
data-ref="addButton"
|
||||
type="button"
|
||||
>
|
||||
+ Ajouter une ligne
|
||||
</button>
|
||||
</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">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
{# DATE #}
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-xs text-slate-400 font-medium">
|
||||
{{ quote.createdAt|date('d/m/Y') }}
|
||||
{{ quote.createa|date('d/m/Y') }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
'signée': 'Signé'
|
||||
} %}
|
||||
|
||||
{% set currentStatus = quote.status|lower %}
|
||||
{% set currentStatus = quote.state|lower %}
|
||||
|
||||
<span class="px-3 py-1.5 rounded-lg border text-[8px] font-black uppercase tracking-[0.15em] whitespace-nowrap {{ statusClasses[currentStatus] ?? 'text-slate-400 bg-slate-500/10 border-slate-500/20' }}">
|
||||
{% if currentStatus == 'en attends de signature' %}
|
||||
@@ -95,7 +95,7 @@
|
||||
{# MONTANT #}
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class="text-sm font-black text-white">
|
||||
{{ quote.totalTtc|number_format(2, ',', ' ') }}€
|
||||
{{ 0|number_format(2, ',', ' ') }}€
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -103,19 +103,19 @@
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
{# Modifier #}
|
||||
<a href="{{ path('app_crm_devis_edit', {id: quote.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5">
|
||||
<a href="{{ path('app_crm_devis_add', {id: quote.id}) }}" class="p-2 bg-blue-600/10 hover:bg-blue-600 text-blue-500 hover:text-white rounded-xl transition-all border border-blue-500/20 shadow-lg shadow-blue-600/5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||
</a>
|
||||
|
||||
{# PDF #}
|
||||
<a href="{{ path('app_crm_devis_pdf', {id: quote.id}) }}" target="_blank" class="p-2 bg-emerald-600/10 hover:bg-emerald-600 text-emerald-500 hover:text-white rounded-xl transition-all border border-emerald-500/20 shadow-lg shadow-emerald-600/5">
|
||||
<a href="{{ path('app_crm_devis_add', {id: quote.id}) }}" target="_blank" class="p-2 bg-emerald-600/10 hover:bg-emerald-600 text-emerald-500 hover:text-white rounded-xl transition-all border border-emerald-500/20 shadow-lg shadow-emerald-600/5">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||
</a>
|
||||
|
||||
{# Delete #}
|
||||
<a href="{{ path('app_crm_devis_delete', {id: quote.id}) }}?_token={{ csrf_token('delete' ~ quote.id) }}"
|
||||
<a href="{{ path('app_crm_devis_add', {id: quote.id}) }}?_token={{ csrf_token('delete' ~ quote.id) }}"
|
||||
data-turbo-method="post"
|
||||
data-turbo-confirm="Confirmer la suppression du devis {{ quote.ref }} ?"
|
||||
data-turbo-confirm="Confirmer la suppression du devis {{ quote.num }} ?"
|
||||
class="p-2 bg-rose-500/10 hover:bg-rose-600 text-rose-500 hover:text-white rounded-xl transition-all border border-rose-500/20">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user