Compare commits

..

1 commit

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

View file

@ -1,9 +1,5 @@
ARG PHP_8_5_TAG=latest
FROM composer:2.9.2 AS vendor
WORKDIR /app
ARG GITLAB_USER
ARG GITLAB_PASSWORD
@ -12,7 +8,7 @@ COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
@ -32,10 +28,9 @@ COPY resources/ /app/resources/
WORKDIR /app
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci --no-audit --no-fund --loglevel=error && npm run prod
RUN npm ci && npm run prod
ARG PHP_8_5_TAG
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.4:170625-130159
ARG USER=www-data

View file

@ -8,7 +8,7 @@
],
"license": "MIT",
"require": {
"php": "^8.5",
"php": "^8.4",
"24slides/laravel-saml2": "^2.4",
"guzzlehttp/guzzle": "^7.5.0",
"laravel/framework": "^12.0",
@ -46,9 +46,7 @@
"dealerdirect/phpcodesniffer-composer-installer": true,
"php-http/discovery": true
},
"gitlab-domains": [
"lab.s-web.fr"
]
"gitlab-domains": ["lab.s-web.fr"]
},
"extra": {
"laravel": {

1571
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,7 @@ data:
APP_DEBUG: "${APP_APP_DEBUG}"
APP_URL: "https://${APP_DOMAIN}"
LOG_CHANNEL: "k8s"
DB_HOST: "${APP_GLOBAL_OVH_MYSQL_HOST}"
DB_PORT: "${APP_GLOBAL_OVH_MYSQL_PORT}"
DB_HOST: "${APP_DB_HOST}"
DB_DATABASE: "${APP_DB_DATABASE}"
DB_USERNAME: "${APP_DB_USERNAME}"
DB_PASSWORD: "${APP_DB_PASSWORD}"
@ -84,9 +83,9 @@ spec:
imagePullSecrets:
- name: docker-registry-credentials
securityContext:
runAsUser: 82
runAsGroup: 82
fsGroup: 82
runAsUser: 33
runAsGroup: 33
fsGroup: 33
fsGroupChangePolicy: "OnRootMismatch"
volumes:
- name: storage
@ -172,8 +171,8 @@ spec:
persistentVolumeClaim:
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
securityContext:
runAsUser: 82
runAsGroup: 82
fsGroup: 82
runAsUser: 33
runAsGroup: 33
fsGroup: 33
fsGroupChangePolicy: "OnRootMismatch"
restartPolicy: OnFailure

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');

View file

@ -35,26 +35,18 @@ Route::prefix('bo')->name('bo.')->middleware('admin-secure')->group(function ()
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
// * Utilisateurs BO
Route::resource('objectives', ObjectivesController::class)->except([
'create',
'edit'
'create', 'edit'
]);
Route::resources([
'users' => UsersController::class,
'employees' => EmployeesController::class,
'home-sliders' => HomeSlidersController::class,
'articles' => ArticlesController::class,
'cotations' => CotationsController::class,
'public-pdfs' => PublicPdfsController::class,
]);
// * ARTICLES
Route::resource(
'articles',
ArticlesController::class,
)->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
Route::resource('customers', CustomerController::class)
->only(['index', 'edit', 'update']);
->only(['index', 'edit', 'update']);
// *ordres
Route::get(