Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Abdel LAMRANI
f4df06e1c7 fix order and add field needed 2026-10-07 10:41:56 +02:00
3 changed files with 398 additions and 50 deletions

View file

@ -1,7 +1,7 @@
<template>
<div class="pro--objectifs">
<div class="objectifs--form">
<h2 class="post-title">Fixer un objectif de prix</h2>
<h2 class="post-title">Objectif de prix</h2>
<transition name="fade">
<div class="alert alert-success" v-show="processing">
@ -24,50 +24,95 @@
</div>
</transition>
<form>
<div class="flex row">
<select name="produit" id="produit" v-model="objectif.produit" @change="populate">
<option value="--">Selectionner un produit*</option>
<option v-for="produit in enums.produit" :key="produit.value" :value="produit.value">
{{ produit.value }}
</option>
</select>
<form @submit.prevent="saveData">
<div class="objectifs--grid">
<label for="produit" class="col-start">Produit * :</label>
<select name="produit" id="produit" v-model="objectif.produit" @change="populate">
<option value="--">Choisir un produit</option>
<option v-for="produit in enums.produit" :key="produit.value" :value="produit.value">
{{ produit.value }}
</option>
</select>
<select name="qualite" v-model="objectif.qualite">
<option value="--">Qualité*</option>
<option v-for="qualite in qualiteEnums" :key="qualite.value" :value="qualite.value">
{{ qualite.value }}
</option>
</select>
</div>
<label for="campagne">Campagne * :</label>
<select name="campagne" id="campagne" v-model="objectif.campagne" @change="populate">
<option value="--">Choisir une campagne</option>
<option v-for="campagne in campagneEnums" :key="campagne.value" :value="campagne.value">
{{ campagne.value }}
</option>
</select>
<div class="flex row">
<select name="campagne" v-model="objectif.campagne" @change="populate">
<option value="--">Campagne*</option>
<option v-for="campagne in campagneEnums" :key="campagne.value" :value="campagne.value">
{{ campagne.value }}
</option>
</select>
<label for="qualite" class="col-start">Qualité * :</label>
<select name="qualite" id="qualite" v-model="objectif.qualite">
<option value="--">Choisir une qualité</option>
<option v-for="qualite in qualiteEnums" :key="qualite.value" :value="qualite.value">
{{ qualite.value }}
</option>
</select>
<select name="echeance" v-model="objectif.echeance">
<option value="--">Échéance*</option>
<option v-for="echeance in echeanceEnums" :key="echeance.value" :value="echeance.value">
{{ echeance.value }}
</option>
</select>
</div>
<label for="quantite_liste" class="col-start">Quantité * :</label>
<select id="quantite_liste" name="quantite_liste" v-model.number="objectif.quantite">
<option :value="null">Multiple de 30 T</option>
<option v-for="q in QUANTITES" :key="q" :value="q">{{ q }} T</option>
</select>
<div class="flex row">
<input type="number" name="quantite" v-model.number="objectif.quantite" min="0" placeholder="Quantité*">
<input type="number" name="prix" v-model.number="objectif.prix" min="0" step="0.01" placeholder="Prix (€)*">
</div>
<span class="objectifs--ou">ou</span>
<input type="number" name="quantite" class="objectifs--wide" v-model.number="objectif.quantite" min="0" placeholder="Saisie libre*">
<div class="validate">
<button class="first-btn" @click.prevent="saveData" :disabled="processing">
Enregistrer l'objectif
</button>
</div>
</form>
<label for="prix" class="col-start">Prix * :</label>
<div class="objectifs--prix">
<input type="number" id="prix" name="prix" class="objectifs--wide" v-model.number="objectif.prix" min="0" step="0.01" placeholder="Prix (€/TM)*">
<span>€/TM</span>
</div>
<label for="echeance">Échéance * :</label>
<select name="echeance" id="echeance" v-model="objectif.echeance">
<option value="--">Choisir une échéance</option>
<option v-for="echeance in echeanceEnums" :key="echeance.value" :value="echeance.value">
{{ echeance.value }}
</option>
</select>
</div>
<div class="objectifs--line">
<div class="objectifs--field">
<label for="volume_restant">Volume restant à commercialiser :</label>
<input type="number" id="volume_restant" name="volume_restant"
v-model.number="objectif.volume_restant" min="0" placeholder="Saisie libre">
</div>
</div>
<div class="objectifs--line">
<div class="objectifs--field grow">
<label for="observations">Observations :</label>
<input type="text" id="observations" name="observations"
v-model="objectif.observations" placeholder="Saisie libre">
</div>
<button type="submit" class="first-btn" :disabled="processing">OK</button>
</div>
</form>
</div>
<!-- RESUME -->
<div class="objectifs--resume" v-if="myobjectives.length">
<hr class="objectifs--divider">
<h3>Résumé :</h3>
<ul>
<li v-for="(item, i) in myobjectives" :key="item.id ?? i" class="objectif--ligne">
<span class="objectif--texte"
:class="{ editable: isEditable(item) }"
:title="isEditable(item) ? 'Cliquer pour modifier' : ''"
@click="editItem(item)">
Campagne {{ item.campagne }} {{ item.quantite }}TM {{ item.produit }} {{ item.prix }}€
</span>
<span class="objectif--statut" :class="statusClass(item)">
{{ statusLabel(item) }}
</span>
<button type="button" class="objectif--delete" @click="removeItem(i)" aria-label="Supprimer">X</button>
</li>
</ul>
<p class="objectifs--hint">(Modifiable en cliquant sur la ligne)</p>
</div>
</div>
</template>
@ -76,11 +121,21 @@
import { ref } from "vue"
import axios from "axios"
import {
ObjectiveStatus,
OBJECTIVE_STATUS_LABELS,
OBJECTIVE_STATUS_CLASSES,
} from "../../types/ObjectiveStatus"
const props = defineProps<{ json: string }>()
const QUANTITES = Array.from({ length: 20 }, (_, i) => (i + 1) * 30)
const initial = JSON.parse(props.json)
initial.dbobjectif.quantite = null
initial.dbobjectif.volume_restant = null
initial.dbobjectif.observations ??= ""
const objectif = ref<any>(structuredClone(initial.dbobjectif))
const myobjectives = ref<any[]>(initial.dbmyObjectives)
@ -94,7 +149,6 @@ const success = ref(false)
const errors = ref<string[]>([])
const response = ref("")
function populate(event: Event) {
const target = event.target as HTMLSelectElement
@ -107,14 +161,47 @@ function populate(event: Event) {
campagneEnums.value = enums.value.campagne[target.value] || []
break
case "campagne":
case "campagne": {
const selectProduit = objectif.value.produit
echeanceEnums.value =
enums.value.echeance?.[selectProduit]?.[target.value] || []
break
}
}
}
function resetForm() {
objectif.value = structuredClone(initial.dbobjectif)
qualiteEnums.value = []
campagneEnums.value = []
echeanceEnums.value = []
}
function isEditable(item: any): boolean {
return statusId(item) === ObjectiveStatus.EnCours
}
function editItem(item: any) {
if (!isEditable(item)) return
objectif.value = JSON.parse(JSON.stringify(item))
qualiteEnums.value = enums.value.qualite?.[item.produit] || []
campagneEnums.value = enums.value.campagne?.[item.produit] || []
echeanceEnums.value = enums.value.echeance?.[item.produit]?.[item.campagne] || []
errors.value = []
}
function statusId(item: any): ObjectiveStatus {
return item.status?.id ?? item.status_id ?? ObjectiveStatus.EnCours
}
function statusLabel(item: any): string {
return OBJECTIVE_STATUS_LABELS[statusId(item)] ?? "En cours"
}
function statusClass(item: any): string {
return OBJECTIVE_STATUS_CLASSES[statusId(item)] ?? "en-cours"
}
function checkForm(): boolean {
errors.value = []
@ -158,6 +245,7 @@ function removeItem(index: number) {
if (result === 0 || result === 2) {
myobjectives.value.splice(index, 1)
if (String(objectif.value.id) === String(item.id)) resetForm()
response.value = "Suppression terminée !"
} else {
response.value = "Erreur : Suppression impossible"
@ -177,6 +265,8 @@ function removeItem(index: number) {
function saveData() {
if (!checkForm()) return
const editingId = objectif.value.id ?? null
processing.value = true
success.value = false
errors.value = []
@ -187,17 +277,25 @@ function saveData() {
objectif: objectif.value,
})
.then((res) => {
const savedId = editingId ?? res.data.id
const index = myobjectives.value.findIndex(
(o) => String(o.id) === String(savedId)
)
if (index !== -1) {
myobjectives.value[index] = res.data
} else {
myobjectives.value.unshift(res.data)
}
resetForm()
success.value = true
response.value = "Sauvegarde terminée !"
myobjectives.value.push(res.data)
objectif.value = structuredClone(initial.dbobjectif)
qualiteEnums.value = []
campagneEnums.value = []
echeanceEnums.value = []
setTimeout(() => (success.value = false), 3000)
})
.catch(() => {
errors.value.push("Erreur lors de la sauvegarde.")
.catch((err) => {
response.value = "Erreur lors de la sauvegarde :"
errors.value = [err.response?.data?.message ?? "Erreur lors de la sauvegarde."]
})
.finally(() => {
processing.value = false
@ -214,4 +312,232 @@ function saveData() {
.fade-leave-to {
opacity: 0;
}
.objectifs--required {
margin-left: 1.5rem;
font-size: .6em;
font-weight: normal;
color: #d00;
}
.objectifs--required span {
font-size: 1.5em;
vertical-align: middle;
}
.objectifs--grid {
display: grid;
grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr);
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.objectifs--grid > label {
margin: 0;
white-space: nowrap;
}
.objectifs--grid .col-start {
grid-column: 1;
}
.objectifs--grid select,
.objectifs--grid input {
width: 100%;
min-width: 0;
margin: 0;
}
/* Qualité sur toute la largeur */
.objectifs--grid #qualite {
grid-column: 2 / -1;
}
.objectifs--grid .objectifs--ou {
justify-self: end;
}
.objectifs--prix {
display: flex;
align-items: center;
gap: .5rem;
}
.pro--objectifs .objectifs--form form .objectifs--grid input[type="number"].objectifs--wide {
flex: 1;
width: 100%;
max-width: none;
min-width: 0;
}
.objectifs--line {
display: flex;
align-items: center;
gap: 1rem 2rem;
margin-bottom: 1rem;
}
.objectifs--field {
display: flex;
align-items: center;
gap: .5rem;
}
.objectifs--field.grow {
flex: 1;
min-width: 0;
}
.objectifs--field.grow input {
flex: 1;
min-width: 0;
}
.objectifs--field label {
margin: 0;
white-space: nowrap;
}
.objectifs--line .first-btn {
margin-left: auto;
}
.objectifs--resume {
margin-top: -25px;
padding: 20px;
}
.objectifs--divider {
margin: 0 0 1rem;
border: 0;
border-top: 2px solid #000;
opacity: 1;
}
.objectifs--resume h3 {
margin-top: 0;
text-decoration: underline;
}
.objectifs--resume ul {
list-style: none;
margin: 0;
padding: 0;
}
.objectif--ligne {
display: flex;
align-items: center;
gap: 1rem;
padding: .25rem 0;
}
.objectif--texte {
flex: 1;
}
.objectif--texte.editable {
cursor: pointer;
}
.objectif--texte.editable:hover {
text-decoration: underline;
}
.objectif--statut.en-cours {
color: #1a9a3c;
}
.objectif--statut.atteint {
color: #d00;
}
.objectif--statut.depassee {
color: #888;
}
.objectif--delete {
border: 0;
background: none;
color: #d00;
font-weight: bold;
cursor: pointer;
padding-inline: 5px;
}
.objectifs--hint {
margin-bottom: 0;
font-style: italic;
font-size: .9em;
}
/* Tablette */
@media (max-width: 768px) {
.objectifs--grid {
grid-template-columns: max-content minmax(0, 1fr);
}
/* Tous les libellés reviennent en colonne 1 */
.objectifs--grid > label {
grid-column: 1;
}
.objectifs--grid .objectifs--ou {
justify-self: start;
}
.objectifs--line {
flex-wrap: wrap;
}
.objectifs--line .first-btn {
margin-left: 0;
}
}
/* Mobile*/
@media (max-width: 480px) {
.objectifs--grid {
grid-template-columns: minmax(0, 1fr);
gap: .5rem;
}
.objectifs--grid > label,
.objectifs--grid .col-start,
.objectifs--grid #qualite {
grid-column: 1;
}
.objectifs--grid > label {
margin-top: .5rem;
}
.objectifs--field {
flex-direction: column;
align-items: stretch;
width: 100%;
}
.objectifs--field label {
white-space: normal;
}
.pro--objectifs .objectifs--form form #volume_restant {
width: 100%;
}
.objectifs--line .first-btn {
width: 100%;
}
.objectif--ligne {
flex-wrap: wrap;
gap: .25rem 1rem;
}
.objectif--texte {
flex: 1 1 100%;
}
}
</style>

View file

@ -0,0 +1,22 @@
export enum ObjectiveStatus {
EnCours = 1,
Atteint = 2,
EcheanceDepassee = 3,
}
export interface ObjectiveStatusData {
id: ObjectiveStatus
label: string
}
export const OBJECTIVE_STATUS_LABELS: Record<ObjectiveStatus, string> = {
[ObjectiveStatus.EnCours]: "En cours",
[ObjectiveStatus.Atteint]: "Atteint",
[ObjectiveStatus.EcheanceDepassee]: "Échéance dépassée",
}
export const OBJECTIVE_STATUS_CLASSES: Record<ObjectiveStatus, string> = {
[ObjectiveStatus.EnCours]: "en-cours",
[ObjectiveStatus.Atteint]: "atteint",
[ObjectiveStatus.EcheanceDepassee]: "depassee",
}

View file

@ -154,7 +154,7 @@
@endsection
@push('scripts')
<script>
<script nonce="{{ $nonce ?? '' }}">
document.addEventListener('DOMContentLoaded', () => {
const select = document.querySelector('select[name=mps]');
const blocs = document.querySelectorAll('.cotations--grid .mp--bloc');