diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 1ed59be..02ce9dd 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,24 +4,17 @@ - @@ -649,7 +650,8 @@ - diff --git a/AGENTS.md b/AGENTS.md index 58d6072..1b20d25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ Frontend conventions - Nuxt SSR disabled; Tailwind used. - Layout in `frontend/layouts/default.vue`: max width `1050px`, header full width. - Tailwind custom color palette is `primary` (e.g. `bg-primary-500`). +- Global font stack uses Helvetica via Tailwind (`font-sans`) and `frontend/assets/css/main.css`. - API composable in `frontend/composables/useApi.ts` with `get/post/put/patch/delete` and default JSON/PATCH content types. - API errors/success toasts can be customized via `toastErrorMessage`/`toastSuccessMessage` or i18n keys `toastErrorKey`/`toastSuccessKey`. Global method fallbacks use `errors.http.*` keys. - `useApi` uses `useNuxtApp().$i18n` (not `useI18n`) to avoid setup-only constraint in service calls. @@ -44,3 +45,11 @@ Environment & routing Notes - Do not add a GET that creates resources; use POST + PATCH. - Keep endpoints in plural (API Platform convention). +- New reference data added: + - Reception types (`reception_type`, fields: `label`, `code`), selectable on reception form. + - Suppliers (`supplier`) with addresses (`address`, fields: `label`, `street`, `postal_code`, `city`, `country_code` ISO2), via `supplier_address` join table. + - Trucks (`truck`, field: `name`), linked to receptions. + - Carriers (`carrier`, fields: `name`, nullable `code`), Drivers (`driver`, fields: `name`, `carrier_id`), Vehicles (`vehicle`, fields: `plate`, `carrier_id`, `truck_id`) used for LIOT logic. + - Reception links: `reception_type_id`, `supplier_id`, `address_id`, `truck_id`, `carrier_id`, `driver_id`, `user_id`. + - Address exposes `fullAddress` via getter for display. + - LIOT behavior in reception form: if carrier code = `LIOT`, show driver + vehicle selects and hide manual license plate input; vehicle list filters by truck type and carrier; selected vehicle sets `license_plate`. diff --git a/frontend/components/reception/reception-weight.vue b/frontend/components/reception/reception-weight.vue index 46c58f6..cf6c7a9 100644 --- a/frontend/components/reception/reception-weight.vue +++ b/frontend/components/reception/reception-weight.vue @@ -33,14 +33,13 @@ @click="printReceipt" >Générer le bon - - diff --git a/frontend/composables/usePdfPrinter.ts b/frontend/composables/usePdfPrinter.ts index 7a6b4a8..747c684 100644 --- a/frontend/composables/usePdfPrinter.ts +++ b/frontend/composables/usePdfPrinter.ts @@ -1,40 +1,29 @@ -import type { Ref } from 'vue' -import { useApi } from '~/composables/useApi' - -type PrintFrameRef = Ref +import {useApi} from '~/composables/useApi' export const usePdfPrinter = () => { - const api = useApi() + const api = useApi() + const receptionStore = useReceptionStore() + const currentReception = receptionStore.current - const printPdf = async (url: string, frameRef: PrintFrameRef): Promise => { - if (!import.meta.client) { - return + const printPdf = async (url: string): Promise => { + if (!import.meta.client) { + return + } + + const blob = await api.getBlob(url) + const blobUrl = URL.createObjectURL(blob) + const a = document.createElement('a'); + a.href = url; + // nom du fichier à changer par les infos du store pinia + a.download = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}`; + document.body.appendChild(a); + a.click(); + a.remove(); + window.open(blobUrl, '_blank', 'noopener,noreferrer') + setTimeout(() => URL.revokeObjectURL(blobUrl), 60000) } - const frame = frameRef.value - if (!frame) { - return + return { + printPdf } - - // On charge le PDF en blob pour rester en same-origin dans l'iframe. - const blob = await api.getBlob(url) - const blobUrl = URL.createObjectURL(blob) - - const tryPrint = () => { - frame.contentWindow?.focus() - frame.contentWindow?.print() - } - - frame.onload = () => { - tryPrint() - // On libere l'URL blob apres l'impression. - setTimeout(() => URL.revokeObjectURL(blobUrl), 2000) - } - frame.src = blobUrl - setTimeout(tryPrint, 1200) - } - - return { - printPdf - } } diff --git a/frontend/services/dto/reception-data.ts b/frontend/services/dto/reception-data.ts index c49da24..6b831b5 100644 --- a/frontend/services/dto/reception-data.ts +++ b/frontend/services/dto/reception-data.ts @@ -8,6 +8,7 @@ import type { DriverData } from '~/services/dto/driver-data' export interface ReceptionData { id: number + identificationNumber?: string | null licensePlate: string | null weights?: WeightEntryData[] | null receptionDate: string diff --git a/migrations/Version20260128000100.php b/migrations/Version20260128000100.php new file mode 100644 index 0000000..8dde194 --- /dev/null +++ b/migrations/Version20260128000100.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE reception ADD identification_number VARCHAR(20) DEFAULT NULL'); + $this->addSql("UPDATE reception SET identification_number = 'N-BR-' || LPAD(id::text, 4, '0') WHERE identification_number IS NULL"); + $this->addSql('CREATE UNIQUE INDEX UNIQ_reception_identification_number ON reception (identification_number)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX UNIQ_reception_identification_number'); + $this->addSql('ALTER TABLE reception DROP identification_number'); + } +} diff --git a/src/Entity/Address.php b/src/Entity/Address.php index ee21674..18e2481 100644 --- a/src/Entity/Address.php +++ b/src/Entity/Address.php @@ -35,19 +35,19 @@ class Address private ?int $id = null; #[ORM\Column(length: 120)] - #[Groups(['address:read', 'supplier:read'])] + #[Groups(['address:read', 'supplier:read', 'reception:read'])] private string $label = ''; #[ORM\Column(length: 180)] - #[Groups(['address:read', 'supplier:read'])] + #[Groups(['address:read', 'supplier:read', 'reception:read'])] private string $street = ''; #[ORM\Column(name: 'postal_code', length: 20)] - #[Groups(['address:read', 'supplier:read'])] + #[Groups(['address:read', 'supplier:read', 'reception:read'])] private string $postalCode = ''; #[ORM\Column(length: 120)] - #[Groups(['address:read', 'supplier:read'])] + #[Groups(['address:read', 'supplier:read', 'reception:read'])] private string $city = ''; #[ORM\Column(name: 'country_code', length: 2)] diff --git a/src/Entity/Reception.php b/src/Entity/Reception.php index fdef52d..11c58c6 100644 --- a/src/Entity/Reception.php +++ b/src/Entity/Reception.php @@ -17,6 +17,7 @@ use App\State\ReceptionWeighingProvider; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Context; use Symfony\Component\Serializer\Attribute\Groups; @@ -78,6 +79,10 @@ class Reception #[Groups(['reception:read', 'reception:write'])] private ?string $licensePlate = null; + #[ORM\Column(length: 20, unique: true, nullable: true)] + #[Groups(['reception:read'])] + private ?string $identificationNumber = null; + #[ORM\Column(options: ['default' => 0])] #[Groups(['reception:read', 'reception:write'])] private int $currentStep = 0; @@ -155,6 +160,18 @@ class Reception return $this->licensePlate; } + public function getIdentificationNumber(): ?string + { + return $this->identificationNumber; + } + + public function setIdentificationNumber(?string $identificationNumber): self + { + $this->identificationNumber = $identificationNumber; + + return $this; + } + public function setLicensePlate(?string $licensePlate): self { $this->licensePlate = $licensePlate; @@ -321,4 +338,30 @@ class Reception $this->receptionDate = new DateTimeImmutable(); } } + + #[ORM\PostPersist] + public function initializeIdentificationNumber(PostPersistEventArgs $args): void + { + if (null !== $this->identificationNumber) { + return; + } + + if (null === $this->id) { + return; + } + + $number = sprintf('N-BR-%04d', $this->id); + $this->identificationNumber = $number; + + $args->getObjectManager() + ->getConnection() + ->executeStatement( + 'UPDATE reception SET identification_number = :number WHERE id = :id', + [ + 'number' => $number, + 'id' => $this->id, + ] + ) + ; + } } diff --git a/templates/reception_voucher.html.twig b/templates/reception_voucher.html.twig index 4405ac1..c1c0553 100644 --- a/templates/reception_voucher.html.twig +++ b/templates/reception_voucher.html.twig @@ -7,7 +7,7 @@ @page { margin: 56px 56px; } body{ - font-family: Arial, sans-serif; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; margin:0; color:#000; @@ -19,7 +19,7 @@ .red{ color:red; } .company-block{ - font-size:13px; + font-size:14px; text-align:left; line-height:1.25; } @@ -36,15 +36,30 @@ text-align:center; font-size: 18pt; font-weight: 700; - margin: 0 0 4mm 0; + margin: 64px 0 15px 0; + } + + .info-table { + margin-bottom: 32px; + width:100%; + border-collapse:collapse; + table-layout:fixed; + } + + .info-table th { + font-size: 16px; + } + + table{ + width:100%; + border-collapse: collapse; } - table{ width:100%; border-collapse: collapse; } th, td{ border:1px solid #333; padding:4px 6px; vertical-align: top; - font-size: 9pt; + font-size: 12px; } th{ text-align:center; font-weight:700; } @@ -52,7 +67,17 @@ .layout, .layout td{ border:none !important; padding:0; } /* GRAND TABLEAU : verrouillage dompdf */ - .bigtable{ table-layout: fixed; } + .bigtable{ + table-layout: fixed; + } + .bigtable th, + .bigtable td{ + font-size: 16px; + } + .bigtable-notes{ + font-size: 14px; + line-height: 1.25; + } /* ligne “filler” pour forcer la hauteur comme l'exemple */ .fill td{ @@ -60,32 +85,9 @@ height: 75mm; /* ajuste si besoin */ } - /* Bloc IDTF comme l’exemple */ - table.idtf{ - width: 350px; /* ou 100% si tu préfères */ - border-collapse: collapse; - border: 1px solid #333 !important; /* bordure extérieure */ - margin-top: 12px; /* ~3mm */ - font-size: 8.5pt; - table-layout: fixed; - } - - /* IMPORTANT: on cible td DANS table.idtf et on force */ - table.idtf td{ - border: 1px solid #333 !important; - padding: 3px 5px; - vertical-align: top; - } - - /* Largeurs en % (pas de mm) */ - table.idtf td.n{ width: 8%; text-align:center; } - table.idtf td.prod{ width: 52%; } - table.idtf td.net{ width: 20%; } - table.idtf td.date{ width: 20%; } - - .signature-title{ font-size:9pt; margin-bottom:2mm; } + .signature-title{ font-size:12px; margin-bottom:2mm; } .signature-box{ height: 22mm; margin-bottom: 4mm; } - .meta{ font-size: 9pt; line-height: 1.35; } + .meta{ font-size: 16px; line-height: 1.35; } @@ -95,33 +97,26 @@ - @@ -130,18 +125,18 @@
BON DE RECEPTION
-
- - - -
- -
- SA LIOT Châtellerault
- Site de Châtellerault
+ SCEA LES NAUDS
14 Allée d’Argenson
Z.I Nord – Secteur Est
86100 CHATELLERAULT
- TEL : 05 49 20 09 10 – Fax : 05 49 85 37 82
+ TEL : 05 49 20 09 10
Email : lpc.contacts@lpc-liot.fr
- RCS Châtellerault B 339 505 612 + RCS Châtellerault B 444 262 455
-
- Nom de l'entreprise


- Adresse de l'entreprise +
+
+ {{ reception.supplier.name }}
+ {{ reception.address.street }}
+ {{ reception.address.postalCode }} {{ reception.address.city }}
+
- + - + - + - +
Code fournisseurCode fournisseur DateN° réceptionN° réception
XXX{{ reception.supplier.name }} {{ reception.receptionDate|date('d/m/Y') }} 86-BR-XXXX{{ reception.identificationNumber }}
@@ -150,20 +145,17 @@ - - - + + - - - - - - - + +
CodeDésignationQté livrée (kg)DésignationQté livrée (kg)
M + MAÏS sec

-
+
{% set grossWeight = null %} {% set tareWeight = null %} @@ -179,7 +171,7 @@
+ {% if grossWeight and tareWeight %} {{ grossWeight.weight - tareWeight.weight }} {% else %} @@ -190,9 +182,8 @@
@@ -202,32 +193,16 @@
-
- Transporteur : Nom du transporteur
- Mode de livraison : Fond-mouvant
- Immatriculation : {{ reception.licensePlate }}

- Poids annoncé : XXXXX kg +
+ Transporteur : {{ reception.carrier.name }}
+ Mode de livraison : {{ reception.truck.name }}
+ Immatriculation : {{ reception.licensePlate }}

- - - - - - - - - - - -
1 - Produit : Nom du produit
- N° IDTF : 4000XX -
Nettoyage : ADate : 14/01/2026
Signature :
-
Ets Liot :
+
Les Nauds :
Transporteur :