feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception
This commit is contained in:
25
.idea/workspace.xml
generated
25
.idea/workspace.xml
generated
@@ -4,15 +4,15 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : finalisation de l'étape 1 "Réception" (formulaire)">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/AGENTS.md" beforeDir="false" afterPath="$PROJECT_DIR$/AGENTS.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/ui/pdf-printer.vue" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-unloading.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/composables/usePdfPrinter.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/usePdfPrinter.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/Address.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Address.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/Reception.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Reception.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/templates/reception_voucher.html.twig" beforeDir="false" afterPath="$PROJECT_DIR$/templates/reception_voucher.html.twig" afterDir="false" />
|
||||
</list>
|
||||
@@ -278,7 +278,8 @@
|
||||
<workItem from="1768894030675" duration="83922000" />
|
||||
<workItem from="1769413136483" duration="58000" />
|
||||
<workItem from="1769413279223" duration="40490000" />
|
||||
<workItem from="1769612160652" duration="12834000" />
|
||||
<workItem from="1769612160652" duration="23952000" />
|
||||
<workItem from="1769696465294" duration="4048000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
@@ -576,7 +577,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769529522614</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="38" />
|
||||
<task id="LOCAL-00038" summary="feat : ajout du numéro identification des receptions et ajustement du bon de reception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769676223697</created>
|
||||
<option name="number" value="00038" />
|
||||
<option name="presentableId" value="LOCAL-00038" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769676223697</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="39" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -626,7 +635,6 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="feat : ajout de l'authentification avec lexik" />
|
||||
<MESSAGE value="fix : correction de l'accès au swagger en mode dev qui n'était plus accessible" />
|
||||
<MESSAGE value="feat : ajout de la conf pour le déploiement en recette" />
|
||||
<MESSAGE value="fix : fix de la conf pour le déploiement en recette" />
|
||||
@@ -651,7 +659,8 @@
|
||||
<MESSAGE value="fix : modification de la conf du bundle ednotif" />
|
||||
<MESSAGE value="feat : update du CHANGELOG.md" />
|
||||
<MESSAGE value="feat : finalisation de l'étape 1 "Réception" (formulaire)" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : finalisation de l'étape 1 "Réception" (formulaire)" />
|
||||
<MESSAGE value="feat : ajout du numéro identification des receptions et ajustement du bon de reception" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout du numéro identification des receptions et ajustement du bon de reception" />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
||||
@@ -10,6 +10,7 @@ Backend conventions
|
||||
- API Platform operations are defined on Doctrine entities.
|
||||
- Reception entity is in `src/Entity/Reception.php`, with custom weigh endpoint `/receptions/weigh`.
|
||||
- Reception fields: `date_reception`, `license_plate`, `current_step` (default 0), `is_valid` (default false).
|
||||
- Reception also has `identification_number` (auto `N-BR-####`), `merchandise_type`, `merchandise_detail`, `buildings` (M2M), and `pellet_buildings` (via `reception_pellet_building`).
|
||||
- `date_reception` is set by the UI, stored as `DateTimeImmutable`, serialized as `Y-m-d`.
|
||||
- Weight entity (`src/Entity/Weight.php`) is 1–N with Reception, each row stores `type` (`gross` or `tare`), `dsd`, `weight`, `weighed_at` (all nullable except `type`).
|
||||
- Weigh endpoint `/receptions/weigh` returns `PontBasculeReading` with `dsd`, `weight`, `weighedAt` (formatted `Y-m-d`).
|
||||
@@ -29,6 +30,7 @@ Frontend conventions
|
||||
- Zod is used for form validation (e.g. `frontend/components/reception/reception-form.vue`); shared helpers live in `frontend/utils/zod-errors.ts`.
|
||||
- Weighing logic is shared via `frontend/composables/useWeighing.ts`.
|
||||
- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`.
|
||||
- Step 2 uses `frontend/components/reception/reception-product-received.vue` for merchandise selection; type codes in `frontend/utils/constants.ts`.
|
||||
- Active nav styles in header use `NuxtLink` with `custom` slot.
|
||||
- Reusable UI components live under `frontend/components/ui/` and are auto-imported with `Ui` prefix (e.g. `UiLoadingDots`).
|
||||
- Service layer lives in `frontend/services/` with typed DTOs in `frontend/services/dto/`.
|
||||
@@ -47,6 +49,8 @@ Notes
|
||||
- Keep endpoints in plural (API Platform convention).
|
||||
- New reference data added:
|
||||
- Reception types (`reception_type`, fields: `label`, `code`), selectable on reception form.
|
||||
- Merchandise types (`merchandise_type`, fields: `label`, `code`) and pellet types (`pellet_type`, fields: `label`, `code`).
|
||||
- Buildings (`building`, fields: `label`, `code`) and reception allocations (`reception_building` M2M, `reception_pellet_building` unique on reception/pellet/building).
|
||||
- Suppliers (`supplier`) with addresses (`address`, fields: `label`, `street`, `postal_code`, `city`, `country_code` ISO2), via `supplier_address` join table.
|
||||
- Trucks (`truck`, field: `name`), linked to receptions.
|
||||
- Carriers (`carrier`, fields: `name`, nullable `code`), Drivers (`driver`, fields: `name`, `carrier_id`), Vehicles (`vehicle`, fields: `plate`, `carrier_id`, `truck_id`) used for LIOT logic.
|
||||
|
||||
232
frontend/components/reception/reception-product-received.vue
Normal file
232
frontend/components/reception/reception-product-received.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-16">
|
||||
<!-- @TODO voir pour séparer dans un composant au moment de l'implémentation des Bovins -->
|
||||
<div
|
||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
||||
class="flex flex-col gap-8 items-center w-full">
|
||||
<h1 class="text-4xl uppercase font-bold">Sélectionner des marchandises réceptionnnées</h1>
|
||||
<div class="flex flex-col w-[550px]">
|
||||
<label for="merchandise-type" class="font-bold uppercase text-xl mb-2">Type de marchandises</label>
|
||||
<select
|
||||
id="merchandise-type"
|
||||
v-model="selectedMerchandiseTypeId"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent cursor-pointer"
|
||||
:class="selectedMerchandiseTypeId ? 'text-black' : 'text-neutral-400'"
|
||||
>
|
||||
<option value="" disabled class="text-neutral-400">Sélectionner</option>
|
||||
<option
|
||||
v-for="type in merchandiseTypes"
|
||||
:key="type.id"
|
||||
:value="String(type.id)"
|
||||
class="text-black"
|
||||
>
|
||||
{{ type.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && isAutres"
|
||||
class="flex flex-col w-full max-w-[550px]"
|
||||
>
|
||||
<label for="merchandise-detail" class="font-bold uppercase text-xl mb-2">Préciser</label>
|
||||
<input
|
||||
id="merchandise-detail"
|
||||
v-model="merchandiseDetail"
|
||||
placeholder="Précisions complémentaires"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
class="border-b border-black text-xl pb-[6px] bg-transparent"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||
class="flex gap-4 w-[550px] justify-evenly"
|
||||
>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
>
|
||||
<label
|
||||
class="flex items-center gap-2 text-xl"
|
||||
>
|
||||
<input
|
||||
v-model="selectedBuildingIds"
|
||||
type="checkbox"
|
||||
:value="String(building.id)"
|
||||
>
|
||||
<span>{{ building.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && isGranule"
|
||||
class="flex flex-col gap-10 w-full max-w-[900px]"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||
<p class="font-bold uppercase">{{ type.label }}</p>
|
||||
<label
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
class="flex items-center gap-2 text-lg"
|
||||
>
|
||||
<input
|
||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||
type="checkbox"
|
||||
:value="String(building.id)"
|
||||
>
|
||||
<span>{{ building.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
@click="goNext"
|
||||
>Peser</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getBuildingList } from '~/services/building'
|
||||
import { getMerchandiseTypeList } from '~/services/merchandise-type'
|
||||
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||
import { getPelletTypeList } from '~/services/pellet-type'
|
||||
import {
|
||||
createReceptionPelletBuilding,
|
||||
deleteReceptionPelletBuilding,
|
||||
getReceptionPelletBuildingList
|
||||
} from '~/services/reception-pellet-building'
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
import { MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES } from '~/utils/constants'
|
||||
|
||||
const receptionStore = useReceptionStore()
|
||||
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
||||
const buildings = ref<BuildingData[]>([])
|
||||
const pelletTypes = ref<PelletTypeData[]>([])
|
||||
const selectedMerchandiseTypeId = ref('')
|
||||
const selectedBuildingIds = ref<string[]>([])
|
||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||
const merchandiseDetail = ref('')
|
||||
|
||||
const selectedMerchandiseType = computed(() =>
|
||||
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value)
|
||||
)
|
||||
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
|
||||
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
|
||||
|
||||
onMounted(async () => {
|
||||
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
||||
getMerchandiseTypeList(),
|
||||
getBuildingList(),
|
||||
getPelletTypeList()
|
||||
])
|
||||
merchandiseTypes.value = merchandiseTypeList
|
||||
buildings.value = buildingList
|
||||
pelletTypes.value = pelletTypeList
|
||||
|
||||
const currentId = receptionStore.current?.merchandiseType?.id
|
||||
if (currentId) {
|
||||
selectedMerchandiseTypeId.value = String(currentId)
|
||||
}
|
||||
merchandiseDetail.value = receptionStore.current?.merchandiseDetail ?? ''
|
||||
|
||||
selectedBuildingIds.value =
|
||||
receptionStore.current?.buildings?.map((building) => String(building.id)) ?? []
|
||||
|
||||
const existingPelletSelections = receptionStore.current?.pelletBuildings ?? []
|
||||
const selectionMap: Record<string, string[]> = {}
|
||||
for (const selection of existingPelletSelections) {
|
||||
const pelletTypeId = String(selection.pelletType.id)
|
||||
const buildingId = String(selection.building.id)
|
||||
if (!selectionMap[pelletTypeId]) {
|
||||
selectionMap[pelletTypeId] = []
|
||||
}
|
||||
selectionMap[pelletTypeId].push(buildingId)
|
||||
}
|
||||
for (const pelletType of pelletTypes.value) {
|
||||
const key = String(pelletType.id)
|
||||
if (!selectionMap[key]) {
|
||||
selectionMap[key] = []
|
||||
}
|
||||
}
|
||||
selectedPelletBuildingIds.value = selectionMap
|
||||
})
|
||||
|
||||
async function goNext() {
|
||||
if (!receptionStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
merchandiseType: selectedMerchandiseTypeId.value
|
||||
? `/api/merchandise_types/${selectedMerchandiseTypeId.value}`
|
||||
: null,
|
||||
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
|
||||
buildings: isGranule.value
|
||||
? []
|
||||
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
|
||||
currentStep: nextStep
|
||||
})
|
||||
|
||||
if (isGranule.value) {
|
||||
await syncPelletSelections(receptionIri)
|
||||
} else {
|
||||
await clearPelletSelections(receptionIri)
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPelletSelections(receptionIri: string) {
|
||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||
for (const selection of existing) {
|
||||
await deleteReceptionPelletBuilding(selection.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPelletSelections(receptionIri: string) {
|
||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||
const existingMap = new Map<string, number>()
|
||||
for (const selection of existing) {
|
||||
const key = `${selection.pelletType.id}:${selection.building.id}`
|
||||
existingMap.set(key, selection.id)
|
||||
}
|
||||
|
||||
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
|
||||
for (const [pelletTypeId, buildingIds] of Object.entries(selectedPelletBuildingIds.value)) {
|
||||
for (const buildingId of buildingIds) {
|
||||
desiredEntries.push({ pelletTypeId, buildingId })
|
||||
}
|
||||
}
|
||||
|
||||
const desiredKeys = new Set(desiredEntries.map(
|
||||
(entry) => `${entry.pelletTypeId}:${entry.buildingId}`
|
||||
))
|
||||
|
||||
for (const [key, id] of existingMap.entries()) {
|
||||
if (!desiredKeys.has(key)) {
|
||||
await deleteReceptionPelletBuilding(id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of desiredEntries) {
|
||||
const key = `${entry.pelletTypeId}:${entry.buildingId}`
|
||||
if (!existingMap.has(key)) {
|
||||
await createReceptionPelletBuilding({
|
||||
reception: receptionIri,
|
||||
pelletType: `/api/pellet_types/${entry.pelletTypeId}`,
|
||||
building: `/api/buildings/${entry.buildingId}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center mt-[164px] gap-32">
|
||||
<div class="flex gap-8 items-center justify-center">
|
||||
<!--@TODO Prendre en compte que l'on peut aussi décharger de la marchandise-->
|
||||
<h1 class="text-4xl uppercase font-bold">Décharger les bêtes</h1>
|
||||
<UiLoadingDots />
|
||||
</div>
|
||||
<button
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
@click="goNext"
|
||||
>Peser</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
|
||||
const receptionStore = useReceptionStore()
|
||||
|
||||
async function goNext() {
|
||||
if (!receptionStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
currentStep: nextStep
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -6,21 +6,26 @@ export const usePdfPrinter = () => {
|
||||
const currentReception = receptionStore.current
|
||||
|
||||
const printPdf = async (url: string): Promise<void> => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
const blob = await api.getBlob(url);
|
||||
|
||||
const pdfBlob = blob.type === 'application/pdf'
|
||||
? blob
|
||||
: new Blob([blob], { type: 'application/pdf' });
|
||||
|
||||
const blobUrl = URL.createObjectURL(pdfBlob);
|
||||
|
||||
const filename = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}.pdf`;
|
||||
|
||||
const blob = await api.getBlob(url)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
// nom du fichier à changer par les infos du store pinia
|
||||
a.download = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}`;
|
||||
a.href = blobUrl;
|
||||
a.download = filename;
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60000)
|
||||
|
||||
window.open(blobUrl, '_blank', 'noopener,noreferrer');
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,6 +17,20 @@
|
||||
"receptionType": {
|
||||
"list": "Impossible de récupérer la liste des types de réception."
|
||||
},
|
||||
"merchandiseType": {
|
||||
"list": "Impossible de récupérer la liste des types de marchandises."
|
||||
},
|
||||
"building": {
|
||||
"list": "Impossible de récupérer la liste des bâtiments."
|
||||
},
|
||||
"pelletType": {
|
||||
"list": "Impossible de récupérer la liste des types de granulés."
|
||||
},
|
||||
"receptionPelletBuilding": {
|
||||
"list": "Impossible de récupérer la liste des dépôts de granulés.",
|
||||
"create": "Impossible d'enregistrer le dépôt de granulés.",
|
||||
"delete": "Impossible de supprimer le dépôt de granulés."
|
||||
},
|
||||
"supplier": {
|
||||
"list": "Impossible de récupérer la liste des fournisseurs."
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||
<ReceptionUnloading v-if="storeReception?.currentStep === 2"/>
|
||||
<ReceptionProductReceived v-if="storeReception?.currentStep === 2"/>
|
||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
23
frontend/services/building.ts
Normal file
23
frontend/services/building.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
|
||||
export type BuildingListResponse =
|
||||
| BuildingData[]
|
||||
| { 'hydra:member'?: BuildingData[] }
|
||||
|
||||
export async function getBuildingList(): Promise<BuildingData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<BuildingListResponse>('buildings', {}, {
|
||||
toastErrorKey: 'errors.building.list'
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
5
frontend/services/dto/building-data.ts
Normal file
5
frontend/services/dto/building-data.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface BuildingData {
|
||||
id: number
|
||||
label: string
|
||||
code: string
|
||||
}
|
||||
5
frontend/services/dto/merchandise-type-data.ts
Normal file
5
frontend/services/dto/merchandise-type-data.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface MerchandiseTypeData {
|
||||
id: number
|
||||
label: string
|
||||
code: string
|
||||
}
|
||||
5
frontend/services/dto/pellet-type-data.ts
Normal file
5
frontend/services/dto/pellet-type-data.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface PelletTypeData {
|
||||
id: number
|
||||
label: string
|
||||
code: string
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
import type { ReceptionPelletBuildingData } from '~/services/dto/reception-pellet-building-data'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||
import type { AddressData } from '~/services/dto/address-data'
|
||||
@@ -15,6 +18,10 @@ export interface ReceptionData {
|
||||
currentStep: number
|
||||
isValid: boolean
|
||||
receptionType?: ReceptionTypeData | null
|
||||
merchandiseType?: MerchandiseTypeData | null
|
||||
merchandiseDetail?: string | null
|
||||
buildings?: BuildingData[] | null
|
||||
pelletBuildings?: ReceptionPelletBuildingData[] | null
|
||||
user?: UserData | null
|
||||
supplier?: SupplierData | null
|
||||
address?: AddressData | null
|
||||
@@ -37,6 +44,9 @@ export type ReceptionPayload = {
|
||||
currentStep?: number
|
||||
isValid?: boolean
|
||||
receptionType?: string | null
|
||||
merchandiseType?: string | null
|
||||
merchandiseDetail?: string | null
|
||||
buildings?: string[] | null
|
||||
user?: string | null
|
||||
supplier?: string | null
|
||||
address?: string | null
|
||||
|
||||
9
frontend/services/dto/reception-pellet-building-data.ts
Normal file
9
frontend/services/dto/reception-pellet-building-data.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||
|
||||
export interface ReceptionPelletBuildingData {
|
||||
id: number
|
||||
reception?: string
|
||||
building: BuildingData
|
||||
pelletType: PelletTypeData
|
||||
}
|
||||
23
frontend/services/merchandise-type.ts
Normal file
23
frontend/services/merchandise-type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||
|
||||
export type MerchandiseTypeListResponse =
|
||||
| MerchandiseTypeData[]
|
||||
| { 'hydra:member'?: MerchandiseTypeData[] }
|
||||
|
||||
export async function getMerchandiseTypeList(): Promise<MerchandiseTypeData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<MerchandiseTypeListResponse>('merchandise_types', {}, {
|
||||
toastErrorKey: 'errors.merchandiseType.list'
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
23
frontend/services/pellet-type.ts
Normal file
23
frontend/services/pellet-type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||
|
||||
export type PelletTypeListResponse =
|
||||
| PelletTypeData[]
|
||||
| { 'hydra:member'?: PelletTypeData[] }
|
||||
|
||||
export async function getPelletTypeList(): Promise<PelletTypeData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<PelletTypeListResponse>('pellet_types', {}, {
|
||||
toastErrorKey: 'errors.pelletType.list'
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
51
frontend/services/reception-pellet-building.ts
Normal file
51
frontend/services/reception-pellet-building.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { ReceptionPelletBuildingData } from '~/services/dto/reception-pellet-building-data'
|
||||
|
||||
export type ReceptionPelletBuildingListResponse =
|
||||
| ReceptionPelletBuildingData[]
|
||||
| { 'hydra:member'?: ReceptionPelletBuildingData[] }
|
||||
|
||||
export type ReceptionPelletBuildingPayload = {
|
||||
reception: string
|
||||
pelletType: string
|
||||
building: string
|
||||
}
|
||||
|
||||
export async function getReceptionPelletBuildingList(
|
||||
receptionIri: string
|
||||
): Promise<ReceptionPelletBuildingData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<ReceptionPelletBuildingListResponse>(
|
||||
'reception_pellet_buildings',
|
||||
{ reception: receptionIri },
|
||||
{
|
||||
toastErrorKey: 'errors.receptionPelletBuilding.list'
|
||||
}
|
||||
)
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export async function createReceptionPelletBuilding(
|
||||
payload: ReceptionPelletBuildingPayload
|
||||
): Promise<ReceptionPelletBuildingData> {
|
||||
const api = useApi()
|
||||
return api.post<ReceptionPelletBuildingData>('reception_pellet_buildings', payload, {
|
||||
toastErrorKey: 'errors.receptionPelletBuilding.create'
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteReceptionPelletBuilding(id: number): Promise<void> {
|
||||
const api = useApi()
|
||||
await api.delete<void>(`reception_pellet_buildings/${id}`, {}, {
|
||||
toastErrorKey: 'errors.receptionPelletBuilding.delete'
|
||||
})
|
||||
}
|
||||
8
frontend/utils/constants.ts
Normal file
8
frontend/utils/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const RECEPTION_TYPE_CODES = {
|
||||
MERCHANDISES: 'MARCHANDISES'
|
||||
} as const
|
||||
|
||||
export const MERCHANDISE_TYPE_CODES = {
|
||||
GRANULE: 'GRANULE',
|
||||
AUTRES: 'AUTRES'
|
||||
} as const
|
||||
32
migrations/Version20260128000200.php
Normal file
32
migrations/Version20260128000200.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260128000200 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add merchandise types and link receptions';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE merchandise_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(120) NOT NULL, code VARCHAR(50) NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('ALTER TABLE reception ADD merchandise_type_id INT DEFAULT NULL');
|
||||
$this->addSql('CREATE INDEX IDX_83DC02E3BCAAA7C0 ON reception (merchandise_type_id)');
|
||||
$this->addSql('ALTER TABLE reception ADD CONSTRAINT FK_83DC02E3BCAAA7C0 FOREIGN KEY (merchandise_type_id) REFERENCES merchandise_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE reception DROP CONSTRAINT FK_83DC02E3BCAAA7C0');
|
||||
$this->addSql('DROP INDEX IDX_83DC02E3BCAAA7C0');
|
||||
$this->addSql('ALTER TABLE reception DROP merchandise_type_id');
|
||||
$this->addSql('DROP TABLE merchandise_type');
|
||||
}
|
||||
}
|
||||
48
migrations/Version20260128000300.php
Normal file
48
migrations/Version20260128000300.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260128000300 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add buildings, pellet types, and reception allocations';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE building (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(120) NOT NULL, code VARCHAR(50) NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE TABLE pellet_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(120) NOT NULL, code VARCHAR(50) NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE TABLE reception_building (reception_id INT NOT NULL, building_id INT NOT NULL, PRIMARY KEY(reception_id, building_id))');
|
||||
$this->addSql('CREATE INDEX IDX_46E7F9F23E4A2E34 ON reception_building (reception_id)');
|
||||
$this->addSql('CREATE INDEX IDX_46E7F9F24D2A7E12 ON reception_building (building_id)');
|
||||
$this->addSql('ALTER TABLE reception_building ADD CONSTRAINT FK_46E7F9F23E4A2E34 FOREIGN KEY (reception_id) REFERENCES reception (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE reception_building ADD CONSTRAINT FK_46E7F9F24D2A7E12 FOREIGN KEY (building_id) REFERENCES building (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE TABLE reception_pellet_building (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, reception_id INT NOT NULL, pellet_type_id INT NOT NULL, building_id INT NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_reception_pellet_building ON reception_pellet_building (reception_id, pellet_type_id, building_id)');
|
||||
$this->addSql('CREATE INDEX IDX_5DF3AA933E4A2E34 ON reception_pellet_building (reception_id)');
|
||||
$this->addSql('CREATE INDEX IDX_5DF3AA93955258D ON reception_pellet_building (pellet_type_id)');
|
||||
$this->addSql('CREATE INDEX IDX_5DF3AA934D2A7E12 ON reception_pellet_building (building_id)');
|
||||
$this->addSql('ALTER TABLE reception_pellet_building ADD CONSTRAINT FK_5DF3AA933E4A2E34 FOREIGN KEY (reception_id) REFERENCES reception (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE reception_pellet_building ADD CONSTRAINT FK_5DF3AA93955258D FOREIGN KEY (pellet_type_id) REFERENCES pellet_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE reception_pellet_building ADD CONSTRAINT FK_5DF3AA934D2A7E12 FOREIGN KEY (building_id) REFERENCES building (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE reception_pellet_building DROP CONSTRAINT FK_5DF3AA933E4A2E34');
|
||||
$this->addSql('ALTER TABLE reception_pellet_building DROP CONSTRAINT FK_5DF3AA93955258D');
|
||||
$this->addSql('ALTER TABLE reception_pellet_building DROP CONSTRAINT FK_5DF3AA934D2A7E12');
|
||||
$this->addSql('DROP TABLE reception_pellet_building');
|
||||
$this->addSql('ALTER TABLE reception_building DROP CONSTRAINT FK_46E7F9F23E4A2E34');
|
||||
$this->addSql('ALTER TABLE reception_building DROP CONSTRAINT FK_46E7F9F24D2A7E12');
|
||||
$this->addSql('DROP TABLE reception_building');
|
||||
$this->addSql('DROP TABLE pellet_type');
|
||||
$this->addSql('DROP TABLE building');
|
||||
}
|
||||
}
|
||||
26
migrations/Version20260128000400.php
Normal file
26
migrations/Version20260128000400.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260128000400 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add merchandise detail to reception';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE reception ADD merchandise_detail VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE reception DROP merchandise_detail');
|
||||
}
|
||||
}
|
||||
92
src/Entity/Building.php
Normal file
92
src/Entity/Building.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'building')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
normalizationContext: ['groups' => ['building:read']],
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['building:read']],
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
class Building
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['building:read', 'reception:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Groups(['building:read', 'reception:read'])]
|
||||
private string $label = '';
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
#[Groups(['building:read', 'reception:read'])]
|
||||
private string $code = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, Reception>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Reception::class, mappedBy: 'buildings')]
|
||||
private Collection $receptions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->receptions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): self
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(string $code): self
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Reception>
|
||||
*/
|
||||
public function getReceptions(): Collection
|
||||
{
|
||||
return $this->receptions;
|
||||
}
|
||||
}
|
||||
92
src/Entity/MerchandiseType.php
Normal file
92
src/Entity/MerchandiseType.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'merchandise_type')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
normalizationContext: ['groups' => ['merchandise-type:read']],
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['merchandise-type:read']],
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
class MerchandiseType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['reception:read', 'merchandise-type:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Groups(['reception:read', 'merchandise-type:read'])]
|
||||
private string $label = '';
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
#[Groups(['reception:read', 'merchandise-type:read'])]
|
||||
private string $code = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, Reception>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'merchandiseType', targetEntity: Reception::class)]
|
||||
private Collection $receptions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->receptions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): self
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(string $code): self
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Reception>
|
||||
*/
|
||||
public function getReceptions(): Collection
|
||||
{
|
||||
return $this->receptions;
|
||||
}
|
||||
}
|
||||
71
src/Entity/PelletType.php
Normal file
71
src/Entity/PelletType.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'pellet_type')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
normalizationContext: ['groups' => ['pellet-type:read']],
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['pellet-type:read']],
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
class PelletType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['pellet-type:read', 'reception:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Groups(['pellet-type:read', 'reception:read'])]
|
||||
private string $label = '';
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
#[Groups(['pellet-type:read', 'reception:read'])]
|
||||
private string $code = '';
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): self
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(string $code): self
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,10 @@ class Reception
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
private ?DateTimeImmutable $receptionDate = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['reception:read', 'reception:write'])]
|
||||
private ?string $merchandiseDetail = null;
|
||||
|
||||
#[ORM\OneToMany(targetEntity: Weight::class, mappedBy: 'reception', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[Groups(['reception:read'])]
|
||||
private Collection $weights;
|
||||
@@ -106,6 +110,28 @@ class Reception
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?ReceptionType $receptionType = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'receptions')]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
#[Groups(['reception:read', 'reception:write'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?MerchandiseType $merchandiseType = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Building>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Building::class, inversedBy: 'receptions')]
|
||||
#[ORM\JoinTable(name: 'reception_building')]
|
||||
#[Groups(['reception:read', 'reception:write'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private Collection $buildings;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ReceptionPelletBuilding>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ReceptionPelletBuilding::class, mappedBy: 'reception', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[Groups(['reception:read'])]
|
||||
private Collection $pelletBuildings;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
#[Groups(['reception:read', 'reception:write'])]
|
||||
@@ -147,6 +173,8 @@ class Reception
|
||||
) {
|
||||
$this->receptionDate = $receptionDate;
|
||||
$this->weights = new ArrayCollection();
|
||||
$this->buildings = new ArrayCollection();
|
||||
$this->pelletBuildings = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -218,6 +246,18 @@ class Reception
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMerchandiseDetail(): ?string
|
||||
{
|
||||
return $this->merchandiseDetail;
|
||||
}
|
||||
|
||||
public function setMerchandiseDetail(?string $merchandiseDetail): self
|
||||
{
|
||||
$this->merchandiseDetail = $merchandiseDetail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Weight>
|
||||
*/
|
||||
@@ -238,6 +278,71 @@ class Reception
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMerchandiseType(): ?MerchandiseType
|
||||
{
|
||||
return $this->merchandiseType;
|
||||
}
|
||||
|
||||
public function setMerchandiseType(?MerchandiseType $merchandiseType): self
|
||||
{
|
||||
$this->merchandiseType = $merchandiseType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Building>
|
||||
*/
|
||||
public function getBuildings(): Collection
|
||||
{
|
||||
return $this->buildings;
|
||||
}
|
||||
|
||||
public function addBuilding(Building $building): self
|
||||
{
|
||||
if (!$this->buildings->contains($building)) {
|
||||
$this->buildings->add($building);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeBuilding(Building $building): self
|
||||
{
|
||||
$this->buildings->removeElement($building);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ReceptionPelletBuilding>
|
||||
*/
|
||||
public function getPelletBuildings(): Collection
|
||||
{
|
||||
return $this->pelletBuildings;
|
||||
}
|
||||
|
||||
public function addPelletBuilding(ReceptionPelletBuilding $pelletBuilding): self
|
||||
{
|
||||
if (!$this->pelletBuildings->contains($pelletBuilding)) {
|
||||
$this->pelletBuildings->add($pelletBuilding);
|
||||
$pelletBuilding->setReception($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePelletBuilding(ReceptionPelletBuilding $pelletBuilding): self
|
||||
{
|
||||
if ($this->pelletBuildings->removeElement($pelletBuilding)) {
|
||||
if ($pelletBuilding->getReception() === $this) {
|
||||
$pelletBuilding->setReception(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
|
||||
101
src/Entity/ReceptionPelletBuilding.php
Normal file
101
src/Entity/ReceptionPelletBuilding.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'reception_pellet_building')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_reception_pellet_building', columns: ['reception_id', 'pellet_type_id', 'building_id'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
normalizationContext: ['groups' => ['reception-pellet-building:read']],
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['reception-pellet-building:read']],
|
||||
),
|
||||
new Post(
|
||||
normalizationContext: ['groups' => ['reception-pellet-building:read']],
|
||||
denormalizationContext: ['groups' => ['reception-pellet-building:write']],
|
||||
),
|
||||
new Delete(),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['reception' => 'exact'])]
|
||||
class ReceptionPelletBuilding
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['reception-pellet-building:read', 'reception:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'pelletBuildings')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Groups(['reception-pellet-building:read', 'reception-pellet-building:write'])]
|
||||
private ?Reception $reception = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Groups(['reception-pellet-building:read', 'reception-pellet-building:write', 'reception:read'])]
|
||||
private ?PelletType $pelletType = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Groups(['reception-pellet-building:read', 'reception-pellet-building:write', 'reception:read'])]
|
||||
private ?Building $building = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getReception(): ?Reception
|
||||
{
|
||||
return $this->reception;
|
||||
}
|
||||
|
||||
public function setReception(?Reception $reception): self
|
||||
{
|
||||
$this->reception = $reception;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPelletType(): ?PelletType
|
||||
{
|
||||
return $this->pelletType;
|
||||
}
|
||||
|
||||
public function setPelletType(?PelletType $pelletType): self
|
||||
{
|
||||
$this->pelletType = $pelletType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBuilding(): ?Building
|
||||
{
|
||||
return $this->building;
|
||||
}
|
||||
|
||||
public function setBuilding(?Building $building): self
|
||||
{
|
||||
$this->building = $building;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,10 @@
|
||||
p{ margin:0; }
|
||||
em{ font-style: normal; }
|
||||
|
||||
.red{ color:red; }
|
||||
|
||||
.company-block{
|
||||
font-size:14px;
|
||||
text-align:left;
|
||||
line-height:1.25;
|
||||
line-height:1.3;
|
||||
}
|
||||
|
||||
.box{
|
||||
@@ -36,7 +34,7 @@
|
||||
text-align:center;
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
margin: 64px 0 15px 0;
|
||||
margin: 64px 0 20px 0;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
@@ -63,30 +61,60 @@
|
||||
}
|
||||
th{ text-align:center; font-weight:700; }
|
||||
|
||||
/* tables de layout (sans bordures) */
|
||||
.layout, .layout td{ border:none !important; padding:0; }
|
||||
|
||||
/* GRAND TABLEAU : verrouillage dompdf */
|
||||
.bigtable-wrap{
|
||||
border: 1px solid #000;
|
||||
height: 425px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bigtable{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.bigtable th,
|
||||
.bigtable td{
|
||||
font-size: 16px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.bigtable thead th{ border-top: 0; }
|
||||
.bigtable tbody tr:last-child td{ border-bottom: 0; }
|
||||
|
||||
.bigtable tr th:first-child,
|
||||
.bigtable tr td:first-child{ border-left: 0; }
|
||||
|
||||
.bigtable tr th:last-child,
|
||||
.bigtable tr td:last-child{ border-right: 0; }
|
||||
|
||||
.bigtable thead th{ border-bottom: 0; }
|
||||
.bigtable tbody tr:first-child td{ border-top: 1px solid #333; }
|
||||
|
||||
.bigtable-notes{
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* ligne “filler” pour forcer la hauteur comme l'exemple */
|
||||
.fill td{
|
||||
border-top:none;
|
||||
height: 75mm; /* ajuste si besoin */
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.footer-block{ page-break-inside: avoid; }
|
||||
|
||||
.signature-title{ font-size:12px; margin-bottom:2mm; }
|
||||
.signature-box{ height: 22mm; margin-bottom: 4mm; }
|
||||
.signature-box {
|
||||
height: 22mm;
|
||||
margin-bottom: 4mm;
|
||||
border: 0.5px solid #000;
|
||||
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.meta{ font-size: 16px; line-height: 1.35; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -104,7 +132,7 @@
|
||||
14 Allée d’Argenson<br>
|
||||
Z.I Nord – Secteur Est<br>
|
||||
86100 CHATELLERAULT<br>
|
||||
TEL : 05 49 20 09 10<br>
|
||||
Tel. : 05 49 20 09 10<br>
|
||||
Email : lpc.contacts@lpc-liot.fr<br>
|
||||
RCS Châtellerault B 444 262 455
|
||||
</td>
|
||||
@@ -113,7 +141,7 @@
|
||||
</td>
|
||||
|
||||
<td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;">
|
||||
<div style="display:inline-block; width:75mm;">
|
||||
<div style="display:inline-block; width:75mm; line-height:1.3;">
|
||||
<strong>{{ reception.supplier.name }}</strong><br>
|
||||
<span>{{ reception.address.street }}</span><br>
|
||||
<span>{{ reception.address.postalCode }} {{ reception.address.city }}</span>
|
||||
@@ -140,20 +168,20 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- GRAND TABLEAU -->
|
||||
<table class="bigtable" style="margin-bottom:10px; width:100%; border-collapse:collapse; table-layout:fixed;">
|
||||
<div class="bigtable-wrap">
|
||||
<table class="bigtable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:75%; text-align:center;">Désignation</th>
|
||||
<th style="width:25%; text-align:right; white-space:nowrap;">Qté livrée (kg)</th>
|
||||
<th style="width:25%; text-align:center; white-space:nowrap;">Qté livrée (kg)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:75%;">
|
||||
<strong class="red">MAÏS sec</strong><br><br>
|
||||
<strong>{{ reception.receptionType.label }}</strong><br><br>
|
||||
|
||||
<div class="bigtable-notes">
|
||||
{% set grossWeight = null %}
|
||||
@@ -171,7 +199,7 @@
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="width:25%; text-align:right; white-space:nowrap;">
|
||||
<td style="width:25%; text-align:center; white-space:nowrap;">
|
||||
{% if grossWeight and tareWeight %}
|
||||
{{ grossWeight.weight - tareWeight.weight }}
|
||||
{% else %}
|
||||
@@ -180,17 +208,60 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- filler : garde le grand bloc haut comme sur l'exemple -->
|
||||
<tr class="fill">
|
||||
<td style="width:75%;"></td>
|
||||
<td style="width:25%;"></td>
|
||||
<tr class="border-bottom">
|
||||
<td>
|
||||
<strong>
|
||||
{% if reception.merchandiseType %}
|
||||
{{ reception.merchandiseType.label }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</strong>
|
||||
<br><br>
|
||||
|
||||
<div class="bigtable-notes">
|
||||
{% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %}
|
||||
<p><strong>Précision</strong> : {{ reception.merchandiseDetail }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %}
|
||||
{% set pelletGroups = {} %}
|
||||
{% for selection in reception.pelletBuildings %}
|
||||
{% set pelletLabel = selection.pelletType.label %}
|
||||
{% if pelletGroups[pelletLabel] is not defined %}
|
||||
{% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %}
|
||||
{% endif %}
|
||||
{% set pelletGroups = pelletGroups|merge({
|
||||
(pelletLabel): pelletGroups[pelletLabel]|merge([selection.building.label])
|
||||
}) %}
|
||||
{% endfor %}
|
||||
|
||||
{% for pelletLabel, buildingLabels in pelletGroups %}
|
||||
<p><strong>{{ pelletLabel }}</strong> : {{ buildingLabels|join(', ') }}</p>
|
||||
{% else %}
|
||||
<p>Aucun dépôt de granulés renseigné.</p>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% set buildingLabels = [] %}
|
||||
{% for building in reception.buildings %}
|
||||
{% set buildingLabels = buildingLabels|merge([building.label]) %}
|
||||
{% endfor %}
|
||||
{% if buildingLabels %}
|
||||
<p><strong>Ferme</strong> : {{ buildingLabels|join(', ') }}</p>
|
||||
{% else %}
|
||||
<p>Aucun bâtiment renseigné.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- BAS : meta à gauche / signatures à droite (sans float) -->
|
||||
<table class="layout">
|
||||
<!-- BAS : meta à gauche / signatures à droite -->
|
||||
<table class="layout footer-block">
|
||||
<tr>
|
||||
<td style="width:60%; padding-right:8mm; vertical-align:top;">
|
||||
<div class="meta">
|
||||
|
||||
Reference in New Issue
Block a user