-
Nuxt OK ✅
+
+
Liste des receptions
+
+ -
+ Réception numéro {{ reception.id}}
+
+
diff --git a/frontend/pages/reception/[[id]].vue b/frontend/pages/reception/[[id]].vue
new file mode 100644
index 0000000..74fc1f6
--- /dev/null
+++ b/frontend/pages/reception/[[id]].vue
@@ -0,0 +1,42 @@
+
+ {{ errorMessage }}
+ Chargement...
+
+
+
+
diff --git a/frontend/services/dto/reception-data.ts b/frontend/services/dto/reception-data.ts
new file mode 100644
index 0000000..06c614a
--- /dev/null
+++ b/frontend/services/dto/reception-data.ts
@@ -0,0 +1,9 @@
+export interface ReceptionData {
+ id: number
+ dsd: number | null
+ licensePlate: string | null
+ weight: number | null
+ receptionDate: string
+ currentStep: number
+ isValid: boolean
+}
diff --git a/frontend/services/dto/weight-data.ts b/frontend/services/dto/weight-data.ts
new file mode 100644
index 0000000..eb04e26
--- /dev/null
+++ b/frontend/services/dto/weight-data.ts
@@ -0,0 +1,5 @@
+export interface WeightData {
+ weight: number | null
+ dsd: number | null
+ receptionDate: string
+}
diff --git a/frontend/services/reception.ts b/frontend/services/reception.ts
new file mode 100644
index 0000000..68698eb
--- /dev/null
+++ b/frontend/services/reception.ts
@@ -0,0 +1,50 @@
+import { useApi } from '~/composables/useApi'
+import type { ReceptionData } from '~/services/dto/reception-data'
+import type { WeightData } from '~/services/dto/weight-data'
+
+const api = useApi()
+
+export async function getReceptionList() {
+ try {
+ return await api.get
(`receptions`)
+ } catch (error) {
+ console.error(error.message, error)
+ return error
+ }
+}
+
+export async function getReception(id: number) {
+ try {
+ return await api.get(`receptions/${id}`)
+ } catch (error) {
+ console.error(error.message, error)
+ return error
+ }
+}
+
+export async function createReception(payload: Partial = {}) {
+ try {
+ return await api.post('receptions', payload)
+ } catch (error) {
+ console.error(error.message, error)
+ return error
+ }
+}
+
+export async function updateReception(id: number, payload: Partial) {
+ try {
+ return await api.patch(`receptions/${id}`, payload)
+ } catch (error) {
+ console.error(error.message, error)
+ return error
+ }
+}
+
+export async function getWeight(): Promise {
+ try {
+ return await api.get('receptions/weigh')
+ } catch (error) {
+ console.error(error.message, error)
+ return error
+ }
+}
diff --git a/frontend/stores/reception.ts b/frontend/stores/reception.ts
new file mode 100644
index 0000000..3a72772
--- /dev/null
+++ b/frontend/stores/reception.ts
@@ -0,0 +1,72 @@
+import { defineStore } from 'pinia'
+import type { ReceptionData } from '~/services/dto/reception-data'
+import { createReception, getReception, updateReception } from '~/services/reception'
+
+const isReceptionData = (value: unknown): value is ReceptionData => {
+ return Boolean(value && typeof value === 'object' && 'id' in value)
+}
+
+export const useReceptionStore = defineStore('reception', {
+ state: () => ({
+ current: null as ReceptionData | null,
+ isLoading: false,
+ errorMessage: null as string | null
+ }),
+ actions: {
+ setCurrent(reception: ReceptionData | null) {
+ this.current = reception
+ },
+ clearError() {
+ this.errorMessage = null
+ },
+ async loadReception(id: number) {
+ this.isLoading = true
+ this.errorMessage = null
+ try {
+ const result = await getReception(id)
+ if (!isReceptionData(result)) {
+ this.errorMessage = 'Réception introuvable.'
+ this.current = null
+ return null
+ }
+
+ this.current = result
+ return result
+ } finally {
+ this.isLoading = false
+ }
+ },
+ async createReception() {
+ this.isLoading = true
+ this.errorMessage = null
+ try {
+ const result = await createReception()
+ if (!isReceptionData(result)) {
+ this.errorMessage = 'Impossible de créer la réception.'
+ return null
+ }
+
+ this.current = result
+ return result
+ } finally {
+ this.isLoading = false
+ }
+ },
+ async updateReception(id: number, payload: Partial) {
+ this.isLoading = true
+ this.errorMessage = null
+ try {
+ const result = await updateReception(id, payload)
+ if (!isReceptionData(result)) {
+ this.errorMessage = 'Impossible de mettre à jour la réception.'
+ return null
+ }
+
+ this.current = result
+ return result
+ } finally {
+ this.isLoading = false
+ }
+ }
+ }
+})
diff --git a/migrations/Version20260112000100.php b/migrations/Version20260112000100.php
new file mode 100644
index 0000000..1aab642
--- /dev/null
+++ b/migrations/Version20260112000100.php
@@ -0,0 +1,26 @@
+addSql('CREATE TABLE reception (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, dsd INT DEFAULT NULL, weight DOUBLE PRECISION DEFAULT NULL, weighed_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP TABLE reception');
+ }
+}
diff --git a/migrations/Version20260112000200.php b/migrations/Version20260112000200.php
new file mode 100644
index 0000000..bb2f654
--- /dev/null
+++ b/migrations/Version20260112000200.php
@@ -0,0 +1,29 @@
+addSql('CREATE TABLE weight (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, reception_id INT NOT NULL, gross_weight INT DEFAULT NULL, tare_weight INT DEFAULT NULL, gross_weighed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, tare_weighed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_7B4E3B2304A72F3F ON weight (reception_id)');
+ $this->addSql('ALTER TABLE weight ADD CONSTRAINT FK_7B4E3B2304A72F3F FOREIGN KEY (reception_id) REFERENCES reception (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE weight DROP CONSTRAINT FK_7B4E3B2304A72F3F');
+ $this->addSql('DROP TABLE weight');
+ }
+}
diff --git a/migrations/Version20260112000300.php b/migrations/Version20260112000300.php
new file mode 100644
index 0000000..c219525
--- /dev/null
+++ b/migrations/Version20260112000300.php
@@ -0,0 +1,26 @@
+addSql('ALTER TABLE reception RENAME COLUMN weighed_at TO date_reception');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE reception RENAME COLUMN date_reception TO weighed_at');
+ }
+}
diff --git a/migrations/Version20260112000400.php b/migrations/Version20260112000400.php
new file mode 100644
index 0000000..d735777
--- /dev/null
+++ b/migrations/Version20260112000400.php
@@ -0,0 +1,30 @@
+addSql('ALTER TABLE reception ADD license_plate VARCHAR(20) DEFAULT NULL');
+ $this->addSql('ALTER TABLE reception ADD current_step INT DEFAULT 0 NOT NULL');
+ $this->addSql('ALTER TABLE reception ADD is_valid BOOLEAN DEFAULT FALSE NOT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE reception DROP license_plate');
+ $this->addSql('ALTER TABLE reception DROP current_step');
+ $this->addSql('ALTER TABLE reception DROP is_valid');
+ }
+}
diff --git a/src/Dto/PontBasculeReading.php b/src/Dto/PontBasculeReading.php
new file mode 100644
index 0000000..96f4215
--- /dev/null
+++ b/src/Dto/PontBasculeReading.php
@@ -0,0 +1,31 @@
+dsd;
+ }
+
+ public function getWeight(): ?float
+ {
+ return $this->weight;
+ }
+
+ public function getDatetime(): ?DateTimeImmutable
+ {
+ return $this->datetime;
+ }
+}
diff --git a/src/Entity/Reception.php b/src/Entity/Reception.php
new file mode 100644
index 0000000..e26147e
--- /dev/null
+++ b/src/Entity/Reception.php
@@ -0,0 +1,199 @@
+ ['reception:read']],
+ ),
+ new GetCollection(
+ normalizationContext: ['groups' => ['reception:read']],
+ ),
+ new Post(
+ normalizationContext: ['groups' => ['reception:read']],
+ denormalizationContext: ['groups' => ['reception:write']],
+ ),
+ new Patch(
+ normalizationContext: ['groups' => ['reception:read']],
+ denormalizationContext: ['groups' => ['reception:write']],
+ ),
+ new Get(
+ uriTemplate: '/receptions/weigh',
+ openapi: new OpenApiOperation(
+ summary: 'Fetch the current weight reading',
+ description: 'Queries the pont-bascule and returns the weight data.',
+ ),
+ normalizationContext: ['groups' => ['reception:read']],
+ provider: ReceptionWeighingProvider::class,
+ ),
+ ],
+)]
+class Reception
+{
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
+ #[ORM\Column]
+ #[Groups(['reception:read'])]
+ private ?int $id = null;
+
+ #[ORM\Column(nullable: true)]
+ #[Groups(['reception:read', 'reception:write'])]
+ private ?int $dsd = null;
+
+ #[ORM\Column(type: 'float', nullable: true)]
+ #[Groups(['reception:read', 'reception:write'])]
+ private ?float $weight = null;
+
+ #[ORM\Column(length: 20, nullable: true)]
+ #[Groups(['reception:read', 'reception:write'])]
+ private ?string $licensePlate = null;
+
+ #[ORM\Column(options: ['default' => 0])]
+ #[Groups(['reception:read', 'reception:write'])]
+ private int $currentStep = 0;
+
+ #[ORM\Column(options: ['default' => false])]
+ #[Groups(['reception:read', 'reception:write'])]
+ private bool $isValid = false;
+
+ #[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
+ #[Groups(['reception:read'])]
+ private ?DateTimeImmutable $receptionDate = null;
+
+ #[ORM\OneToOne(targetEntity: Weight::class, mappedBy: 'reception', cascade: ['persist', 'remove'])]
+ private ?Weight $weightEntry = null;
+
+ public function __construct(
+ ?int $dsd = null,
+ ?float $weight = null,
+ ?DateTimeImmutable $receptionDate = null,
+ ) {
+ $this->dsd = $dsd;
+ $this->weight = $weight;
+ $this->receptionDate = $receptionDate;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ #[Groups(['reception:read'])]
+ public function getDsd(): ?int
+ {
+ return $this->dsd;
+ }
+
+ public function setDsd(?int $dsd): self
+ {
+ $this->dsd = $dsd;
+
+ return $this;
+ }
+
+ #[Groups(['reception:read'])]
+ public function getWeight(): ?float
+ {
+ return $this->weight;
+ }
+
+ public function setWeight(?float $weight): self
+ {
+ $this->weight = $weight;
+
+ return $this;
+ }
+
+ #[Groups(['reception:read'])]
+ public function getLicensePlate(): ?string
+ {
+ return $this->licensePlate;
+ }
+
+ public function setLicensePlate(?string $licensePlate): self
+ {
+ $this->licensePlate = $licensePlate;
+
+ return $this;
+ }
+
+ #[Groups(['reception:read'])]
+ public function getCurrentStep(): int
+ {
+ return $this->currentStep;
+ }
+
+ public function setCurrentStep(int $currentStep): self
+ {
+ $this->currentStep = $currentStep;
+
+ return $this;
+ }
+
+ #[Groups(['reception:read'])]
+ public function isValid(): bool
+ {
+ return $this->isValid;
+ }
+
+ public function setIsValid(bool $isValid): self
+ {
+ $this->isValid = $isValid;
+
+ return $this;
+ }
+
+ #[Groups(['reception:read'])]
+ public function getReceptionDate(): ?DateTimeImmutable
+ {
+ return $this->receptionDate;
+ }
+
+ public function setReceptionDate(?DateTimeImmutable $receptionDate): self
+ {
+ $this->receptionDate = $receptionDate;
+
+ return $this;
+ }
+
+ public function getWeightEntry(): ?Weight
+ {
+ return $this->weightEntry;
+ }
+
+ public function setWeightEntry(?Weight $weightEntry): self
+ {
+ $this->weightEntry = $weightEntry;
+
+ if (null !== $weightEntry && $weightEntry->getReception() !== $this) {
+ $weightEntry->setReception($this);
+ }
+
+ return $this;
+ }
+
+ #[ORM\PrePersist]
+ public function initializeReceptionDate(): void
+ {
+ if (null === $this->receptionDate) {
+ $this->receptionDate = new DateTimeImmutable();
+ }
+ }
+}
diff --git a/src/Entity/Weight.php b/src/Entity/Weight.php
new file mode 100644
index 0000000..c446e22
--- /dev/null
+++ b/src/Entity/Weight.php
@@ -0,0 +1,103 @@
+id;
+ }
+
+ public function getReception(): ?Reception
+ {
+ return $this->reception;
+ }
+
+ public function setReception(?Reception $reception): self
+ {
+ $this->reception = $reception;
+
+ if (null !== $reception && $reception->getWeightEntry() !== $this) {
+ $reception->setWeightEntry($this);
+ }
+
+ return $this;
+ }
+
+ public function getGrossWeight(): ?int
+ {
+ return $this->grossWeight;
+ }
+
+ public function setGrossWeight(?int $grossWeight): self
+ {
+ $this->grossWeight = $grossWeight;
+
+ return $this;
+ }
+
+ public function getTareWeight(): ?int
+ {
+ return $this->tareWeight;
+ }
+
+ public function setTareWeight(?int $tareWeight): self
+ {
+ $this->tareWeight = $tareWeight;
+
+ return $this;
+ }
+
+ public function getGrossWeighedAt(): ?DateTimeImmutable
+ {
+ return $this->grossWeighedAt;
+ }
+
+ public function setGrossWeighedAt(?DateTimeImmutable $grossWeighedAt): self
+ {
+ $this->grossWeighedAt = $grossWeighedAt;
+
+ return $this;
+ }
+
+ public function getTareWeighedAt(): ?DateTimeImmutable
+ {
+ return $this->tareWeighedAt;
+ }
+
+ public function setTareWeighedAt(?DateTimeImmutable $tareWeighedAt): self
+ {
+ $this->tareWeighedAt = $tareWeighedAt;
+
+ return $this;
+ }
+}
diff --git a/src/Exception/PontBasculeException.php b/src/Exception/PontBasculeException.php
new file mode 100644
index 0000000..f0587ac
--- /dev/null
+++ b/src/Exception/PontBasculeException.php
@@ -0,0 +1,30 @@
+bypass) {
+ $body = $this->getBypassPayload();
+ } else {
+ try {
+ $response = $this->httpClient->request('POST', $this->endpoint);
+ $body = $response->getContent(false);
+ } catch (TransportExceptionInterface $exception) {
+ throw PontBasculeException::transportFailure($exception->getMessage());
+ }
+ }
+
+ $reading = $this->payloadDecoder->decode($body);
+
+ return new PontBasculeReading(
+ $reading->getDsd(),
+ $reading->getWeight(),
+ new DateTimeImmutable(),
+ );
+ }
+
+ private function getBypassPayload(): string
+ {
+ return '{"ok":true,"busy":false,"mode":"serial","port":"/dev/ttyUSB0","baudrate":9600,"request_hex":"01 10 39 39 4D 0D 0A","response_hex":"01 02 30 34 30 32 30 30 02 30 31 30 30 31 34 32 30 2E 6B 67 20 02 30 32 30 30 30 30 30 30 2E 6B 67 20 02 30 33 30 30 31 34 32 30 2E 6B 67 20 02 39 39 30 30 31 32 31 0D 0A","response_ascii":"\u0001\u0002040200\u000201001420.kg \u000202000000.kg \u000203001420.kg \u00029900121"}';
+ }
+}
diff --git a/src/State/ReceptionWeighingProvider.php b/src/State/ReceptionWeighingProvider.php
new file mode 100644
index 0000000..d6209b8
--- /dev/null
+++ b/src/State/ReceptionWeighingProvider.php
@@ -0,0 +1,34 @@
+pontBasculeService->fetch();
+ } catch (PontBasculeException $exception) {
+ throw new HttpException(500, $exception->getMessage(), $exception);
+ }
+
+ return new Reception(
+ dsd: $result->getDsd(),
+ weight: $result->getWeight(),
+ receptionDate: $result->getDatetime(),
+ );
+ }
+}