From 889e8d6a090b94293b7cfaf23f1e6d10b33066e9 Mon Sep 17 00:00:00 2001 From: AUTIN Tristan Date: Mon, 12 Jan 2026 11:58:46 +0100 Subject: [PATCH 1/9] =?UTF-8?q?feat=20:=20Ajout=20d'un=20layout=20default.?= =?UTF-8?q?vue=20+=20Ajout=20de=20la=20couleur=20primary=20dans=20la=20con?= =?UTF-8?q?f=20tailwind.config.ts=20+Ajout=20d'un=20composable=20pour=20g?= =?UTF-8?q?=C3=A9rer=20les=20appels=20API=20(GET,=20POST)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/ferme.iml | 2 + .idea/php.xml | 2 + .idea/workspace.xml | 287 ++++++++++++++++++++++++++++++++- frontend/app.vue | 4 +- frontend/composables/useApi.ts | 42 +++++ frontend/layouts/default.vue | 38 +++++ frontend/nuxt.config.ts | 5 + frontend/tailwind.config.ts | 22 +++ 8 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 frontend/composables/useApi.ts create mode 100644 frontend/layouts/default.vue create mode 100644 frontend/tailwind.config.ts diff --git a/.idea/ferme.iml b/.idea/ferme.iml index 63cff5a..0e3bfb4 100644 --- a/.idea/ferme.iml +++ b/.idea/ferme.iml @@ -137,6 +137,8 @@ + + diff --git a/.idea/php.xml b/.idea/php.xml index 216185d..322587f 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -143,6 +143,8 @@ + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 1af07c1..5e62f92 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,6 +1,291 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $PROJECT_DIR$/composer.json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "customColor": "", + "associatedIndex": 5 +} + + + + + + + + + + + + + + + + + + + 1767956826164 + + + + + + + + + + + + + file://$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php + 28 + + + + \ No newline at end of file diff --git a/frontend/app.vue b/frontend/app.vue index 4a62f0f..f8eacfa 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -1,3 +1,5 @@ diff --git a/frontend/composables/useApi.ts b/frontend/composables/useApi.ts new file mode 100644 index 0000000..a9abd61 --- /dev/null +++ b/frontend/composables/useApi.ts @@ -0,0 +1,42 @@ +import type { FetchOptions } from 'ofetch' +import { $fetch, FetchError } from 'ofetch' + +export type ApiClient = { + get(path: string, options?: FetchOptions<'json'>): Promise + post(path: string, body?: unknown, options?: FetchOptions<'json'>): Promise +} + +export const useApi = (): ApiClient => { + const config = useRuntimeConfig() + const baseURL = config.public.apiBase ?? '/api' + const client = $fetch.create({ baseURL }) + + return { + get(path: string, options?: FetchOptions<'json'>) { + return client(path, { ...options, method: 'GET' }) + }, + post(path: string, body?: unknown, options?: FetchOptions<'json'>) { + return client(path, { ...options, method: 'POST', body }) + } + } +} + +export const getApiStatus = (error: unknown): number | null => { + if (error && typeof error === 'object') { + if (error instanceof FetchError) { + return error.status ?? error.response?.status ?? null + } + + const maybeResponse = (error as { response?: { status?: number } }).response + if (typeof maybeResponse?.status === 'number') { + return maybeResponse.status + } + + const maybeStatus = (error as { status?: number }).status + if (typeof maybeStatus === 'number') { + return maybeStatus + } + } + + return null +} diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue new file mode 100644 index 0000000..2b33892 --- /dev/null +++ b/frontend/layouts/default.vue @@ -0,0 +1,38 @@ + diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index a8e3e7a..4fc53d1 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -3,6 +3,11 @@ export default defineNuxtConfig({ devtools: { enabled: true }, ssr: false, modules: ['@nuxtjs/tailwindcss'], + runtimeConfig: { + public: { + apiBase: process.env.NUXT_PUBLIC_API_BASE + } + }, typescript: { strict: true } diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..c5beb37 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,22 @@ +import type { Config } from 'tailwindcss' + +export default >{ + theme: { + extend: { + colors: { + primary: { + 50: '#f6f9ea', + 100: '#eaf2cf', + 200: '#d6e3a4', + 300: '#c1d47a', + 400: '#afc85a', + 500: '#9ebb43', + 600: '#7e9735', + 700: '#607228', + 800: '#414d1a', + 900: '#24290d' + } + } + } + } +} -- 2.39.5 From 03638d988b215691326f10c4e8a397b9bc983e16 Mon Sep 17 00:00:00 2001 From: AUTIN Tristan Date: Mon, 12 Jan 2026 14:05:12 +0100 Subject: [PATCH 2/9] =?UTF-8?q?fix=20:=20correction=20du=20useApi=20et=20a?= =?UTF-8?q?jout=20des=20autres=20types=20de=20requ=C3=AAte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/composables/useApi.ts | 54 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/frontend/composables/useApi.ts b/frontend/composables/useApi.ts index a9abd61..186e395 100644 --- a/frontend/composables/useApi.ts +++ b/frontend/composables/useApi.ts @@ -1,9 +1,14 @@ import type { FetchOptions } from 'ofetch' import { $fetch, FetchError } from 'ofetch' +export type AnyObject = Record + export type ApiClient = { - get(path: string, options?: FetchOptions<'json'>): Promise - post(path: string, body?: unknown, options?: FetchOptions<'json'>): Promise + get(url: string, query?: AnyObject, options?: FetchOptions<'json'>): Promise + post(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise + put(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise + patch(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise + delete(url: string, query?: AnyObject, options?: FetchOptions<'json'>): Promise } export const useApi = (): ApiClient => { @@ -11,32 +16,29 @@ export const useApi = (): ApiClient => { const baseURL = config.public.apiBase ?? '/api' const client = $fetch.create({ baseURL }) + const request = ( + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', + url: string, + options: FetchOptions<'json'> = {} + ) => { + return client(url, { ...options, method }) + } + return { - get(path: string, options?: FetchOptions<'json'>) { - return client(path, { ...options, method: 'GET' }) + get(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) { + return request('GET', url, { ...options, query }) }, - post(path: string, body?: unknown, options?: FetchOptions<'json'>) { - return client(path, { ...options, method: 'POST', body }) + post(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) { + return request('POST', url, { ...options, body }) + }, + put(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) { + return request('PUT', url, { ...options, body }) + }, + patch(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) { + return request('PATCH', url, { ...options, body }) + }, + delete(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) { + return request('DELETE', url, { ...options, query }) } } } - -export const getApiStatus = (error: unknown): number | null => { - if (error && typeof error === 'object') { - if (error instanceof FetchError) { - return error.status ?? error.response?.status ?? null - } - - const maybeResponse = (error as { response?: { status?: number } }).response - if (typeof maybeResponse?.status === 'number') { - return maybeResponse.status - } - - const maybeStatus = (error as { status?: number }).status - if (typeof maybeStatus === 'number') { - return maybeStatus - } - } - - return null -} -- 2.39.5 From cfe7baa4ae3bf352e09f61ec8c6f8d3b2adf0776 Mon Sep 17 00:00:00 2001 From: AUTIN Tristan Date: Mon, 12 Jan 2026 18:07:58 +0100 Subject: [PATCH 3/9] =?UTF-8?q?feat=20:=20Ajout=20de=20pinia,=20cr=C3=A9at?= =?UTF-8?q?ion=20de=20la=20table=20weight=20et=20reception=20mise=20en=20p?= =?UTF-8?q?lace=20du=20syst=C3=A8me=20de=20step=20pour=20les=20receptions?= =?UTF-8?q?=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 18 +- AGENTS.md | 35 +++ composer.json | 1 + composer.lock | 180 +++++++++++++++- config/packages/api_platform.yaml | 5 + config/reference.php | 6 +- config/services.yaml | 5 + docker/php/config/vhost.conf | 8 +- .../components/reception/reception-form.vue | 36 ++++ .../components/reception/reception-weight.vue | 41 ++++ frontend/composables/useApi.ts | 13 +- frontend/nuxt.config.ts | 4 +- frontend/package-lock.json | 87 ++++++-- frontend/package.json | 2 + frontend/pages/index.vue | 16 +- frontend/pages/reception/[[id]].vue | 42 ++++ frontend/services/dto/reception-data.ts | 9 + frontend/services/dto/weight-data.ts | 5 + frontend/services/reception.ts | 50 +++++ frontend/stores/reception.ts | 72 +++++++ migrations/Version20260112000100.php | 26 +++ migrations/Version20260112000200.php | 29 +++ migrations/Version20260112000300.php | 26 +++ migrations/Version20260112000400.php | 30 +++ src/Dto/PontBasculeReading.php | 31 +++ src/Entity/Reception.php | 199 ++++++++++++++++++ src/Entity/Weight.php | 103 +++++++++ src/Exception/PontBasculeException.php | 30 +++ src/Service/PontBasculePayloadDecoder.php | 68 ++++++ src/Service/PontBasculeService.php | 51 +++++ src/State/ReceptionWeighingProvider.php | 34 +++ 31 files changed, 1226 insertions(+), 36 deletions(-) create mode 100644 AGENTS.md create mode 100644 frontend/components/reception/reception-form.vue create mode 100644 frontend/components/reception/reception-weight.vue create mode 100644 frontend/pages/reception/[[id]].vue create mode 100644 frontend/services/dto/reception-data.ts create mode 100644 frontend/services/dto/weight-data.ts create mode 100644 frontend/services/reception.ts create mode 100644 frontend/stores/reception.ts create mode 100644 migrations/Version20260112000100.php create mode 100644 migrations/Version20260112000200.php create mode 100644 migrations/Version20260112000300.php create mode 100644 migrations/Version20260112000400.php create mode 100644 src/Dto/PontBasculeReading.php create mode 100644 src/Entity/Reception.php create mode 100644 src/Entity/Weight.php create mode 100644 src/Exception/PontBasculeException.php create mode 100644 src/Service/PontBasculePayloadDecoder.php create mode 100644 src/Service/PontBasculeService.php create mode 100644 src/State/ReceptionWeighingProvider.php diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 5e62f92..f7eb707 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,28 +5,27 @@ - - - + + - - - - + - + + + + + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8806129 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# AGENTS.md + +Project overview +- Symfony 8 + API Platform 4 backend, Nuxt 3 frontend in `frontend/`. +- Apache vhost serves API under `/api` and frontend from `frontend/dist`. +- API base URL on frontend uses `NUXT_PUBLIC_API_BASE` (see `frontend/.env`). + +Backend conventions +- Use English for code identifiers/messages; keep “pont-bascule” as domain term. +- API Platform operations are defined on Doctrine entities. +- Reception entity is in `src/Entity/Reception.php`, with custom weigh endpoint `/receptions/weigh`. +- Reception fields: `dsd`, `weight`, `date_reception`, `license_plate`, `current_step` (default 0), `is_valid` (default false). +- `date_reception` is set by the UI, stored as `DateTimeImmutable`. +- Weight entity (`src/Entity/Weight.php`) is 1–1 with Reception, weights stored as `int` (kg), dates nullable. +- Custom exception: `App\Exception\PontBasculeException` with French messages, mapped to 500 in provider. +- Parsing of pont-bascule payload is in `src/Service/PontBasculePayloadDecoder.php`. +- `config/reference.php` is auto-generated; keep it. + +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`). +- API composable in `frontend/composables/useApi.ts` with `get/post/put/patch/delete` and default JSON/PATCH content types. +- Pinia store: `frontend/stores/reception.ts` is the source of truth for the current reception. +- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`. +- Active nav styles in header use `NuxtLink` with `custom` slot. + +Environment & routing +- Frontend dev server: `npm run dev` in `frontend/`. +- API base for local dev: `http://localhost:8080/api` (set in `frontend/.env` via `NUXT_PUBLIC_API_BASE`). +- CORS handled by Nelmio; `.env` includes `CORS_ALLOW_ORIGIN` regex for localhost. + +Notes +- Do not add a GET that creates resources; use POST + PATCH. +- Keep endpoints in plural (API Platform convention). diff --git a/composer.json b/composer.json index 0a01515..2881725 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "symfony/expression-language": "8.0.*", "symfony/flex": "^2", "symfony/framework-bundle": "8.0.*", + "symfony/http-client": "8.0.*", "symfony/property-access": "8.0.*", "symfony/property-info": "8.0.*", "symfony/runtime": "8.0.*", diff --git a/composer.lock b/composer.lock index bd03af8..fbf6e3d 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "bab4560dec1d36eec0b0aa2284bd8559", + "content-hash": "3e883e3a506afa201779d16a950f4845", "packages": [ { "name": "api-platform/doctrine-common", @@ -4429,6 +4429,180 @@ ], "time": "2025-12-23T14:52:06+00:00" }, + { + "name": "symfony/http-client", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "ea062691009cc2b7bb87734fef20e02671cbd50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ea062691009cc2b7bb87734fef20e02671cbd50b", + "reference": "ea062691009cc2b7bb87734fef20e02671cbd50b", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<3", + "php-http/discovery": "<1.15" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v8.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-23T14:52:06+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, { "name": "symfony/http-foundation", "version": "v8.0.3", @@ -10208,7 +10382,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -10216,6 +10390,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.9.0" } diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 02f295a..4b2b414 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -5,3 +5,8 @@ api_platform: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Origin'] + formats: + json: ['application/json'] + jsonld: ['application/ld+json'] + patch_formats: + json: ['application/merge-patch+json'] diff --git a/config/reference.php b/config/reference.php index 7fe3045..5437970 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1,5 +1,7 @@ , @@ -1378,7 +1380,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * mercure?: bool|array{ * enabled?: bool|Param, // Default: false * hub_url?: scalar|null|Param, // The URL sent in the Link HTTP header. If not set, will default to the URL for MercureBundle's default hub. // Default: null - * include_type?: bool|Param, // Always include @type in updates (including delete ones). // Default: false + * include_type?: bool|Param, // Always include @var in updates (including delete ones). // Default: false * }, * messenger?: bool|array{ * enabled?: bool|Param, // Default: false diff --git a/config/services.yaml b/config/services.yaml index 79b8ce2..1c52e92 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -19,5 +19,10 @@ services: App\: resource: '../src/' + App\Service\PontBasculeService: + arguments: + $endpoint: '%env(PONT_BASCULE_URL)%' + $bypass: '%env(bool:PONT_BASCULE_BYPASS)%' + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/docker/php/config/vhost.conf b/docker/php/config/vhost.conf index e23c743..d300eef 100644 --- a/docker/php/config/vhost.conf +++ b/docker/php/config/vhost.conf @@ -6,9 +6,15 @@ Options FollowSymLinks AllowOverride All Require all granted + + RewriteEngine On + RewriteRule ^index\.php$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^ /api/index.php [L] - AliasMatch "^(/.*)?" "/var/www/html/frontend/dist$1" + AliasMatch "^/(?!api)(.*)$" "/var/www/html/frontend/dist/$1" AllowOverride All Order allow,deny diff --git a/frontend/components/reception/reception-form.vue b/frontend/components/reception/reception-form.vue new file mode 100644 index 0000000..d06c73f --- /dev/null +++ b/frontend/components/reception/reception-form.vue @@ -0,0 +1,36 @@ + + + diff --git a/frontend/components/reception/reception-weight.vue b/frontend/components/reception/reception-weight.vue new file mode 100644 index 0000000..8c8cb8e --- /dev/null +++ b/frontend/components/reception/reception-weight.vue @@ -0,0 +1,41 @@ + + + diff --git a/frontend/composables/useApi.ts b/frontend/composables/useApi.ts index 186e395..d27e341 100644 --- a/frontend/composables/useApi.ts +++ b/frontend/composables/useApi.ts @@ -21,7 +21,18 @@ export const useApi = (): ApiClient => { url: string, options: FetchOptions<'json'> = {} ) => { - return client(url, { ...options, method }) + const needsJsonBody = method === 'POST' || method === 'PUT' + const needsMergePatch = method === 'PATCH' + + const headers = new Headers(options.headers as HeadersInit | undefined) + + if (needsMergePatch && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/merge-patch+json') + } else if (needsJsonBody && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json') + } + + return client(url, { ...options, method, headers }) } return { diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 4fc53d1..0095093 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -2,7 +2,7 @@ export default defineNuxtConfig({ compatibilityDate: '2025-07-15', devtools: { enabled: true }, ssr: false, - modules: ['@nuxtjs/tailwindcss'], + modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'], runtimeConfig: { public: { apiBase: process.env.NUXT_PUBLIC_API_BASE @@ -11,4 +11,4 @@ export default defineNuxtConfig({ typescript: { strict: true } -}) +}) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d9d7b17..cb12af0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,7 +7,9 @@ "name": "frontend", "hasInstallScript": true, "dependencies": { + "@pinia/nuxt": "^0.11.3", "nuxt": "^4.2.2", + "pinia": "^3.0.4", "vue": "^3.5.26", "vue-router": "^4.6.4" }, @@ -56,7 +58,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2678,6 +2679,20 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pinia/nuxt": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.11.3.tgz", + "integrity": "sha512-7WVNHpWx4qAEzOlnyrRC88kYrwnlR/PrThWT0XI1dSNyUAXu/KBv9oR37uCgYkZroqP5jn8DfzbkNF3BtKvE9w==", + "dependencies": { + "@nuxt/kit": "^4.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "pinia": "^3.0.4" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3512,7 +3527,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.26", @@ -3710,7 +3724,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4092,7 +4105,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4206,7 +4218,6 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4396,7 +4407,6 @@ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "license": "MIT", - "peer": true, "dependencies": { "consola": "^3.2.3" } @@ -7966,7 +7976,6 @@ "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.2.2.tgz", "integrity": "sha512-n6oYFikgLEb70J4+K19jAzfx4exZcRSRX7yZn09P5qlf2Z59VNOBqNmaZO5ObzvyGUZ308SZfL629/Q2v2FVjw==", "license": "MIT", - "peer": true, "dependencies": { "@dxup/nuxt": "^0.2.2", "@nuxt/cli": "^3.31.1", @@ -8245,7 +8254,6 @@ "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.102.0.tgz", "integrity": "sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og==", "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "^0.102.0" }, @@ -8469,6 +8477,61 @@ "node": ">=0.10.0" } }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/pinia/node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/pinia/node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/pinia/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -8523,7 +8586,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9073,7 +9135,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -9530,7 +9591,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10341,7 +10401,6 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -11166,7 +11225,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11523,7 +11581,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -11560,7 +11617,6 @@ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^6.6.4" }, @@ -11762,7 +11818,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/frontend/package.json b/frontend/package.json index 8a7e470..6ac10f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,9 @@ "build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist" }, "dependencies": { + "@pinia/nuxt": "^0.11.3", "nuxt": "^4.2.2", + "pinia": "^3.0.4", "vue": "^3.5.26", "vue-router": "^4.6.4" }, diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index eeed3db..51c97a7 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -1,9 +1,21 @@ 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 @@ + + + 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(), + ); + } +} -- 2.39.5 From 6dab1d789a28eafb11dbd5819726f999aaa0c94c Mon Sep 17 00:00:00 2001 From: AUTIN Tristan Date: Tue, 13 Jan 2026 15:52:47 +0100 Subject: [PATCH 4/9] =?UTF-8?q?feat=20:=20Ajout=20de=20zod,=20cr=C3=A9atio?= =?UTF-8?q?n=20d'un=20composant=20de=20chargement=20loading-dots.vue=20et?= =?UTF-8?q?=20finalisation=20du=20flow=20d'une=20reception?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 58 ++++++---- .../components/reception/reception-form.vue | 109 +++++++++++++++--- .../reception/reception-unloading.vue | 30 +++++ .../components/reception/reception-weight.vue | 107 ++++++++++++++--- frontend/components/ui/loading-dots.vue | 50 ++++++++ frontend/package-lock.json | 11 +- frontend/package.json | 3 +- frontend/pages/reception/[[id]].vue | 30 ++--- frontend/services/dto/reception-data.ts | 11 +- frontend/services/dto/weight-data.ts | 2 +- frontend/services/reception.ts | 2 +- frontend/services/weight.ts | 30 +++++ frontend/utils/zod-errors.ts | 17 +++ migrations/Version20260112000500.php | 28 +++++ migrations/Version20260112000600.php | 42 +++++++ src/Dto/PontBasculeReading.php | 13 ++- src/Entity/Reception.php | 82 ++++++------- src/Entity/Weight.php | 77 +++++++++---- src/State/ReceptionWeighingProvider.php | 10 +- 19 files changed, 547 insertions(+), 165 deletions(-) create mode 100644 frontend/components/reception/reception-unloading.vue create mode 100644 frontend/components/ui/loading-dots.vue create mode 100644 frontend/services/weight.ts create mode 100644 frontend/utils/zod-errors.ts create mode 100644 migrations/Version20260112000500.php create mode 100644 migrations/Version20260112000600.php diff --git a/.idea/workspace.xml b/.idea/workspace.xml index f7eb707..b9f3682 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,28 +4,23 @@ + + + + + file://$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php - 28 + 27 + + file://$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php + 22 + diff --git a/frontend/components/reception/reception-form.vue b/frontend/components/reception/reception-form.vue index d06c73f..9bb342e 100644 --- a/frontend/components/reception/reception-form.vue +++ b/frontend/components/reception/reception-form.vue @@ -1,36 +1,107 @@ diff --git a/frontend/components/reception/reception-unloading.vue b/frontend/components/reception/reception-unloading.vue new file mode 100644 index 0000000..bceac93 --- /dev/null +++ b/frontend/components/reception/reception-unloading.vue @@ -0,0 +1,30 @@ + + + diff --git a/frontend/components/reception/reception-weight.vue b/frontend/components/reception/reception-weight.vue index 8c8cb8e..a640bf7 100644 --- a/frontend/components/reception/reception-weight.vue +++ b/frontend/components/reception/reception-weight.vue @@ -1,41 +1,110 @@ 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 @@ 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; } } -- 2.39.5 From 46af62483f59dc88ec4b39fbf471d281c213da3f Mon Sep 17 00:00:00 2001 From: AUTIN Tristan Date: Tue, 13 Jan 2026 16:05:49 +0100 Subject: [PATCH 5/9] =?UTF-8?q?feat=20:=20Ajout=20d'un=20composable=20pour?= =?UTF-8?q?=20la=20pes=C3=A9e=20qui=20sera=20r=C3=A9utilisable=20pour=20l'?= =?UTF-8?q?exp=C3=A9dition,=20ajout=20de=20contrainte=20sur=20les=20entity?= =?UTF-8?q?=20de=20reception=20et=20weight=20pour=20plus=20de=20robustesse?= =?UTF-8?q?=20et=20correction=20de=20la=20class=20active=20des=20liens=20d?= =?UTF-8?q?ans=20la=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 29 +++-- .../components/reception/reception-weight.vue | 81 +++---------- frontend/composables/useWeighing.ts | 109 ++++++++++++++++++ frontend/layouts/default.vue | 7 +- frontend/pages/reception/[[id]].vue | 4 +- src/Entity/Weight.php | 7 ++ 6 files changed, 157 insertions(+), 80 deletions(-) create mode 100644 frontend/composables/useWeighing.ts diff --git a/.idea/workspace.xml b/.idea/workspace.xml index b9f3682..10647e1 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,23 +4,13 @@