-
{{ weightData.weight }} kg
-
DSD : {{ weightData.dsd }}
+
+
+
{{ title }}
+
+
Pont-bascule connecté
+
+
+
+ {{ displayWeight }} kg
+
+
+
DSD
+
{{ displayDsd }}
+
+
+
-
+
+
+
+
+
diff --git a/frontend/components/ui/loading-dots.vue b/frontend/components/ui/loading-dots.vue
new file mode 100644
index 0000000..eca91a2
--- /dev/null
+++ b/frontend/components/ui/loading-dots.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index cb12af0..1add253 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -11,7 +11,8 @@
"nuxt": "^4.2.2",
"pinia": "^3.0.4",
"vue": "^3.5.26",
- "vue-router": "^4.6.4"
+ "vue-router": "^4.6.4",
+ "zod": "^4.3.5"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0"
@@ -11942,6 +11943,14 @@
"engines": {
"node": ">= 14"
}
+ },
+ "node_modules/zod": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
+ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 6ac10f9..62e6346 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,7 +15,8 @@
"nuxt": "^4.2.2",
"pinia": "^3.0.4",
"vue": "^3.5.26",
- "vue-router": "^4.6.4"
+ "vue-router": "^4.6.4",
+ "zod": "^4.3.5"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0"
diff --git a/frontend/pages/reception/[[id]].vue b/frontend/pages/reception/[[id]].vue
index 74fc1f6..a87486a 100644
--- a/frontend/pages/reception/[[id]].vue
+++ b/frontend/pages/reception/[[id]].vue
@@ -1,42 +1,36 @@
{{ errorMessage }}
- Chargement...
+
+
Indicateur d’étapes
+
Mettre en pause
+
-
-
Décharger
-
+
+
+
diff --git a/frontend/services/dto/reception-data.ts b/frontend/services/dto/reception-data.ts
index 06c614a..94b9c42 100644
--- a/frontend/services/dto/reception-data.ts
+++ b/frontend/services/dto/reception-data.ts
@@ -1,9 +1,16 @@
export interface ReceptionData {
id: number
- dsd: number | null
licensePlate: string | null
- weight: number | null
+ weights?: WeightEntryData[] | null
receptionDate: string
currentStep: number
isValid: boolean
}
+
+export interface WeightEntryData {
+ id?: number
+ type: 'gross' | 'tare'
+ dsd: number | null
+ weight: number | null
+ weighedAt: string | null
+}
diff --git a/frontend/services/dto/weight-data.ts b/frontend/services/dto/weight-data.ts
index eb04e26..d4af0a1 100644
--- a/frontend/services/dto/weight-data.ts
+++ b/frontend/services/dto/weight-data.ts
@@ -1,5 +1,5 @@
export interface WeightData {
weight: number | null
dsd: number | null
- receptionDate: string
+ weighedAt: string | null
}
diff --git a/frontend/services/reception.ts b/frontend/services/reception.ts
index 68698eb..3edb1f4 100644
--- a/frontend/services/reception.ts
+++ b/frontend/services/reception.ts
@@ -45,6 +45,6 @@ export async function getWeight(): Promise
{
return await api.get('receptions/weigh')
} catch (error) {
console.error(error.message, error)
- return error
+ throw error
}
}
diff --git a/frontend/services/weight.ts b/frontend/services/weight.ts
new file mode 100644
index 0000000..05fa97c
--- /dev/null
+++ b/frontend/services/weight.ts
@@ -0,0 +1,30 @@
+import { useApi } from '~/composables/useApi'
+import type { WeightEntryData } from '~/services/dto/reception-data'
+
+const api = useApi()
+
+export type WeightPayload = {
+ reception: string
+ type: 'gross' | 'tare'
+ dsd: number | null
+ weight: number | null
+ weighedAt: string | null
+}
+
+export async function createWeight(payload: WeightPayload) {
+ try {
+ return await api.post('weights', payload)
+ } catch (error) {
+ console.error(error.message, error)
+ throw error
+ }
+}
+
+export async function updateWeight(id: number, payload: Partial) {
+ try {
+ return await api.patch(`weights/${id}`, payload)
+ } catch (error) {
+ console.error(error.message, error)
+ throw error
+ }
+}
diff --git a/frontend/utils/zod-errors.ts b/frontend/utils/zod-errors.ts
new file mode 100644
index 0000000..c21b18c
--- /dev/null
+++ b/frontend/utils/zod-errors.ts
@@ -0,0 +1,17 @@
+import type { ZodError } from 'zod'
+
+export type FieldErrors> = Partial>
+
+export const mapZodErrors = >(error: ZodError): FieldErrors => {
+ const flattened = error.flatten().fieldErrors
+ const result: FieldErrors = {}
+
+ for (const key in flattened) {
+ const message = flattened[key]?.[0]
+ if (message) {
+ result[key as keyof T] = message
+ }
+ }
+
+ return result
+}
diff --git a/migrations/Version20260112000500.php b/migrations/Version20260112000500.php
new file mode 100644
index 0000000..0364e8b
--- /dev/null
+++ b/migrations/Version20260112000500.php
@@ -0,0 +1,28 @@
+addSql('ALTER TABLE reception DROP dsd');
+ $this->addSql('ALTER TABLE reception DROP weight');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE reception ADD dsd INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE reception ADD weight DOUBLE PRECISION DEFAULT NULL');
+ }
+}
diff --git a/migrations/Version20260112000600.php b/migrations/Version20260112000600.php
new file mode 100644
index 0000000..3ecd139
--- /dev/null
+++ b/migrations/Version20260112000600.php
@@ -0,0 +1,42 @@
+addSql('DROP INDEX UNIQ_7B4E3B2304A72F3F');
+ $this->addSql('ALTER TABLE weight DROP gross_weight');
+ $this->addSql('ALTER TABLE weight DROP tare_weight');
+ $this->addSql('ALTER TABLE weight DROP gross_weighed_at');
+ $this->addSql('ALTER TABLE weight DROP tare_weighed_at');
+ $this->addSql('ALTER TABLE weight ADD dsd INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE weight ADD weight INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE weight ADD weighed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
+ $this->addSql('ALTER TABLE weight ADD type VARCHAR(10) DEFAULT \'gross\' NOT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE weight DROP dsd');
+ $this->addSql('ALTER TABLE weight DROP weight');
+ $this->addSql('ALTER TABLE weight DROP weighed_at');
+ $this->addSql('ALTER TABLE weight DROP type');
+ $this->addSql('ALTER TABLE weight ADD gross_weight INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE weight ADD tare_weight INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE weight ADD gross_weighed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
+ $this->addSql('ALTER TABLE weight ADD tare_weighed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_7B4E3B2304A72F3F ON weight (reception_id)');
+ }
+}
diff --git a/src/Dto/PontBasculeReading.php b/src/Dto/PontBasculeReading.php
index 96f4215..648c792 100644
--- a/src/Dto/PontBasculeReading.php
+++ b/src/Dto/PontBasculeReading.php
@@ -5,13 +5,20 @@ declare(strict_types=1);
namespace App\Dto;
use DateTimeImmutable;
+use Symfony\Component\Serializer\Attribute\Context;
+use Symfony\Component\Serializer\Attribute\Groups;
+use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
final readonly class PontBasculeReading
{
public function __construct(
+ #[Groups(['reception:weigh:read'])]
private ?int $dsd,
+ #[Groups(['reception:weigh:read'])]
private ?float $weight,
- private ?DateTimeImmutable $datetime = null,
+ #[Groups(['reception:weigh:read'])]
+ #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
+ private ?DateTimeImmutable $weighedAt = null,
) {}
public function getDsd(): ?int
@@ -24,8 +31,8 @@ final readonly class PontBasculeReading
return $this->weight;
}
- public function getDatetime(): ?DateTimeImmutable
+ public function getWeighedAt(): ?DateTimeImmutable
{
- return $this->datetime;
+ return $this->weighedAt;
}
}
diff --git a/src/Entity/Reception.php b/src/Entity/Reception.php
index e26147e..9743b20 100644
--- a/src/Entity/Reception.php
+++ b/src/Entity/Reception.php
@@ -10,10 +10,15 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
+use App\Dto\PontBasculeReading;
use App\State\ReceptionWeighingProvider;
use DateTimeImmutable;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
+use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
@@ -21,6 +26,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new Get(
+ requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['reception:read']],
),
new GetCollection(
@@ -31,6 +37,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
denormalizationContext: ['groups' => ['reception:write']],
),
new Patch(
+ requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['reception:read']],
denormalizationContext: ['groups' => ['reception:write']],
),
@@ -40,7 +47,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
summary: 'Fetch the current weight reading',
description: 'Queries the pont-bascule and returns the weight data.',
),
- normalizationContext: ['groups' => ['reception:read']],
+ normalizationContext: ['groups' => ['reception:weigh:read']],
+ output: PontBasculeReading::class,
provider: ReceptionWeighingProvider::class,
),
],
@@ -53,14 +61,6 @@ class Reception
#[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;
@@ -74,20 +74,19 @@ class Reception
private bool $isValid = false;
#[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
- #[Groups(['reception:read'])]
+ #[Groups(['reception:read', 'reception:write'])]
+ #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeImmutable $receptionDate = null;
- #[ORM\OneToOne(targetEntity: Weight::class, mappedBy: 'reception', cascade: ['persist', 'remove'])]
- private ?Weight $weightEntry = null;
+ #[ORM\OneToMany(targetEntity: Weight::class, mappedBy: 'reception', cascade: ['persist', 'remove'], orphanRemoval: true)]
+ #[Groups(['reception:read'])]
+ private Collection $weights;
public function __construct(
- ?int $dsd = null,
- ?float $weight = null,
?DateTimeImmutable $receptionDate = null,
) {
- $this->dsd = $dsd;
- $this->weight = $weight;
$this->receptionDate = $receptionDate;
+ $this->weights = new ArrayCollection();
}
public function getId(): ?int
@@ -95,32 +94,6 @@ class Reception
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
{
@@ -173,17 +146,30 @@ class Reception
return $this;
}
- public function getWeightEntry(): ?Weight
+ /**
+ * @return Collection
+ */
+ public function getWeights(): Collection
{
- return $this->weightEntry;
+ return $this->weights;
}
- public function setWeightEntry(?Weight $weightEntry): self
+ public function addWeight(Weight $weight): self
{
- $this->weightEntry = $weightEntry;
+ if (!$this->weights->contains($weight)) {
+ $this->weights->add($weight);
+ $weight->setReception($this);
+ }
- if (null !== $weightEntry && $weightEntry->getReception() !== $this) {
- $weightEntry->setReception($this);
+ return $this;
+ }
+
+ public function removeWeight(Weight $weight): self
+ {
+ if ($this->weights->removeElement($weight)) {
+ if ($weight->getReception() === $this) {
+ $weight->setReception(null);
+ }
}
return $this;
diff --git a/src/Entity/Weight.php b/src/Entity/Weight.php
index c446e22..197e8f4 100644
--- a/src/Entity/Weight.php
+++ b/src/Entity/Weight.php
@@ -4,33 +4,62 @@ declare(strict_types=1);
namespace App\Entity;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Patch;
+use ApiPlatform\Metadata\Post;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Attribute\Context;
+use Symfony\Component\Serializer\Attribute\Groups;
+use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\Entity]
#[ORM\Table(name: 'weight')]
+#[ApiResource(
+ operations: [
+ new Get(normalizationContext: ['groups' => ['weight:read']]),
+ new GetCollection(normalizationContext: ['groups' => ['weight:read']]),
+ new Post(
+ normalizationContext: ['groups' => ['weight:read']],
+ denormalizationContext: ['groups' => ['weight:write']],
+ ),
+ new Patch(
+ normalizationContext: ['groups' => ['weight:read']],
+ denormalizationContext: ['groups' => ['weight:write']],
+ ),
+ ],
+)]
class Weight
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
+ #[Groups(['reception:read', 'weight:read'])]
private ?int $id = null;
- #[ORM\OneToOne(inversedBy: 'weightEntry')]
+ #[ORM\ManyToOne(inversedBy: 'weights')]
#[ORM\JoinColumn(nullable: false)]
+ #[Groups(['weight:read', 'weight:write'])]
private ?Reception $reception = null;
#[ORM\Column(nullable: true)]
- private ?int $grossWeight = null;
+ #[Groups(['reception:read', 'weight:read', 'weight:write'])]
+ private ?int $dsd = null;
#[ORM\Column(nullable: true)]
- private ?int $tareWeight = null;
+ #[Groups(['reception:read', 'weight:read', 'weight:write'])]
+ private ?int $weight = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
- private ?DateTimeImmutable $grossWeighedAt = null;
+ #[Groups(['reception:read', 'weight:read', 'weight:write'])]
+ #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
+ private ?DateTimeImmutable $weighedAt = null;
- #[ORM\Column(type: 'datetime_immutable', nullable: true)]
- private ?DateTimeImmutable $tareWeighedAt = null;
+ #[ORM\Column(length: 10)]
+ #[Groups(['reception:read', 'weight:read', 'weight:write'])]
+ private string $type = 'gross';
public function getId(): ?int
{
@@ -46,57 +75,57 @@ class Weight
{
$this->reception = $reception;
- if (null !== $reception && $reception->getWeightEntry() !== $this) {
- $reception->setWeightEntry($this);
+ if (null !== $reception && !$reception->getWeights()->contains($this)) {
+ $reception->addWeight($this);
}
return $this;
}
- public function getGrossWeight(): ?int
+ public function getDsd(): ?int
{
- return $this->grossWeight;
+ return $this->dsd;
}
- public function setGrossWeight(?int $grossWeight): self
+ public function setDsd(?int $dsd): self
{
- $this->grossWeight = $grossWeight;
+ $this->dsd = $dsd;
return $this;
}
- public function getTareWeight(): ?int
+ public function getWeight(): ?int
{
- return $this->tareWeight;
+ return $this->weight;
}
- public function setTareWeight(?int $tareWeight): self
+ public function setWeight(?int $weight): self
{
- $this->tareWeight = $tareWeight;
+ $this->weight = $weight;
return $this;
}
- public function getGrossWeighedAt(): ?DateTimeImmutable
+ public function getWeighedAt(): ?DateTimeImmutable
{
- return $this->grossWeighedAt;
+ return $this->weighedAt;
}
- public function setGrossWeighedAt(?DateTimeImmutable $grossWeighedAt): self
+ public function setWeighedAt(?DateTimeImmutable $weighedAt): self
{
- $this->grossWeighedAt = $grossWeighedAt;
+ $this->weighedAt = $weighedAt;
return $this;
}
- public function getTareWeighedAt(): ?DateTimeImmutable
+ public function getType(): string
{
- return $this->tareWeighedAt;
+ return $this->type;
}
- public function setTareWeighedAt(?DateTimeImmutable $tareWeighedAt): self
+ public function setType(string $type): self
{
- $this->tareWeighedAt = $tareWeighedAt;
+ $this->type = $type;
return $this;
}
diff --git a/src/State/ReceptionWeighingProvider.php b/src/State/ReceptionWeighingProvider.php
index d6209b8..d6b6e0b 100644
--- a/src/State/ReceptionWeighingProvider.php
+++ b/src/State/ReceptionWeighingProvider.php
@@ -6,7 +6,7 @@ namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
-use App\Entity\Reception;
+use App\Dto\PontBasculeReading;
use App\Exception\PontBasculeException;
use App\Service\PontBasculeService;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -17,7 +17,7 @@ final readonly class ReceptionWeighingProvider implements ProviderInterface
private PontBasculeService $pontBasculeService,
) {}
- public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?Reception
+ public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?PontBasculeReading
{
try {
$result = $this->pontBasculeService->fetch();
@@ -25,10 +25,6 @@ final readonly class ReceptionWeighingProvider implements ProviderInterface
throw new HttpException(500, $exception->getMessage(), $exception);
}
- return new Reception(
- dsd: $result->getDsd(),
- weight: $result->getWeight(),
- receptionDate: $result->getDatetime(),
- );
+ return $result;
}
}