Compare commits
1 commit
master
...
issue-24-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4df06e1c7 |
3 changed files with 398 additions and 50 deletions
|
|
@ -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>
|
||||
|
|
|
|||
22
resources/ts/types/ObjectiveStatus.ts
Normal file
22
resources/ts/types/ObjectiveStatus.ts
Normal 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",
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue