613 lines
21 KiB
Vue
613 lines
21 KiB
Vue
<template>
|
|
<div class="container-fluid py-4">
|
|
<!-- Header -->
|
|
|
|
<!-- Form -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card mt-4">
|
|
<div class="card-header pb-0">
|
|
<div class="d-flex align-items-center">
|
|
<p class="font-weight-bold mb-0">Informations du Produit</p>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Success Message -->
|
|
<div v-if="success" class="alert alert-success" role="alert">
|
|
<strong>Succès!</strong> Le produit a été créé avec succès.
|
|
Redirection en cours...
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="text-center">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Création en cours...</span>
|
|
</div>
|
|
<p class="mt-2">Création du produit...</p>
|
|
</div>
|
|
|
|
<!-- Form -->
|
|
<form v-else novalidate @submit.prevent="handleSubmit">
|
|
<!-- Basic Information -->
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label for="nom" class="form-label">Nom du Produit *</label>
|
|
<soft-input
|
|
id="nom"
|
|
v-model="form.nom"
|
|
type="text"
|
|
:class="{ 'is-invalid': validationErrors.nom }"
|
|
required
|
|
placeholder="Entrez le nom du produit"
|
|
/>
|
|
<div v-if="validationErrors.nom" class="invalid-feedback">
|
|
{{ validationErrors.nom[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="reference" class="form-label"
|
|
>Référence *</label
|
|
>
|
|
<soft-input
|
|
id="reference"
|
|
v-model="form.reference"
|
|
type="text"
|
|
:class="{ 'is-invalid': validationErrors.reference }"
|
|
required
|
|
placeholder="Entrez la référence du produit"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.reference"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.reference[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label for="categorie" class="form-label"
|
|
>Catégorie *</label
|
|
>
|
|
<select
|
|
id="categorie"
|
|
v-model="form.categorie_id"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': validationErrors.categorie_id }"
|
|
required
|
|
>
|
|
<option value="">Sélectionnez une catégorie</option>
|
|
<option
|
|
v-for="category in categories"
|
|
:key="category.id"
|
|
:value="category.id"
|
|
>
|
|
{{ category.name }}
|
|
</option>
|
|
</select>
|
|
<div
|
|
v-if="validationErrors.categorie_id"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.categorie_id[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="fabricant" class="form-label">Fabricant</label>
|
|
<soft-input
|
|
id="fabricant"
|
|
v-model="form.fabricant"
|
|
type="text"
|
|
:class="{ 'is-invalid': validationErrors.fabricant }"
|
|
placeholder="Entrez le nom du fabricant"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.fabricant"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.fabricant[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stock Information -->
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="stock_actuel" class="form-label"
|
|
>Stock Actuel *</label
|
|
>
|
|
<soft-input
|
|
id="stock_actuel"
|
|
v-model.number="form.stock_actuel"
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
:class="{ 'is-invalid': validationErrors.stock_actuel }"
|
|
required
|
|
placeholder="0.00"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.stock_actuel"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.stock_actuel[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="stock_minimum" class="form-label"
|
|
>Stock Minimum *</label
|
|
>
|
|
<soft-input
|
|
id="stock_minimum"
|
|
v-model.number="form.stock_minimum"
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
:class="{ 'is-invalid': validationErrors.stock_minimum }"
|
|
required
|
|
placeholder="0.00"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.stock_minimum"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.stock_minimum[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!isIntervention" class="col-md-4">
|
|
<div class="form-group">
|
|
<label for="unite" class="form-label">Unité *</label>
|
|
<select
|
|
id="unite"
|
|
v-model="form.unite"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': validationErrors.unite }"
|
|
required
|
|
>
|
|
<option value="">Sélectionnez une unité</option>
|
|
<option value="pièce">pièce(s)</option>
|
|
<option value="kg">kg</option>
|
|
<option value="g">g</option>
|
|
<option value="l">l</option>
|
|
<option value="ml">ml</option>
|
|
<option value="m">m</option>
|
|
<option value="cm">cm</option>
|
|
<option value="boîte">boîte(s)</option>
|
|
<option value="sachet">sachet(s)</option>
|
|
<option value="bouteille">bouteille(s)</option>
|
|
</select>
|
|
<div v-if="validationErrors.unite" class="invalid-feedback">
|
|
{{ validationErrors.unite[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Price and Dates -->
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label for="prix_unitaire" class="form-label"
|
|
>Prix Unitaire (€) *</label
|
|
>
|
|
<div class="soft-input-group">
|
|
<soft-input
|
|
id="prix_unitaire"
|
|
v-model.number="form.prix_unitaire"
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
:class="{
|
|
'is-invalid': validationErrors.prix_unitaire,
|
|
}"
|
|
required
|
|
placeholder="0.00"
|
|
/>
|
|
|
|
<div
|
|
v-if="validationErrors.prix_unitaire"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.prix_unitaire[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="date_expiration" class="form-label"
|
|
>Date d'Expiration</label
|
|
>
|
|
<soft-input
|
|
id="date_expiration"
|
|
v-model="form.date_expiration"
|
|
type="date"
|
|
:class="{
|
|
'is-invalid': validationErrors.date_expiration,
|
|
}"
|
|
:min="new Date().toISOString().split('T')[0]"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.date_expiration"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.date_expiration[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lot Number -->
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="numero_lot" class="form-label"
|
|
>Numéro de Lot</label
|
|
>
|
|
<soft-input
|
|
id="numero_lot"
|
|
v-model="form.numero_lot"
|
|
type="text"
|
|
:class="{ 'is-invalid': validationErrors.numero_lot }"
|
|
placeholder="Entrez le numéro de lot"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.numero_lot"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.numero_lot[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div v-if="!isIntervention" class="form-group">
|
|
<label for="fournisseur_id" class="form-label"
|
|
>Fournisseur</label
|
|
>
|
|
<select
|
|
id="fournisseur_id"
|
|
v-model="form.fournisseur_id"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': validationErrors.fournisseur_id }"
|
|
>
|
|
<option value="">Sélectionnez un fournisseur</option>
|
|
<option
|
|
v-for="fournisseur in fournisseurs"
|
|
:key="fournisseur.id"
|
|
:value="fournisseur.id"
|
|
>
|
|
{{ fournisseur.name }}
|
|
</option>
|
|
</select>
|
|
<div
|
|
v-if="validationErrors.fournisseur_id"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.fournisseur_id[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Packaging Information -->
|
|
<div v-if="!isIntervention" class="row">
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label for="conditionnement_nom" class="form-label"
|
|
>Nom Conditionnement</label
|
|
>
|
|
<soft-input
|
|
id="conditionnement_nom"
|
|
v-model="form.conditionnement_nom"
|
|
type="text"
|
|
:class="{
|
|
'is-invalid': validationErrors.conditionnement_nom,
|
|
}"
|
|
placeholder="Ex: Carton, Pack..."
|
|
/>
|
|
<div
|
|
v-if="validationErrors.conditionnement_nom"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.conditionnement_nom[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label for="conditionnement_quantite" class="form-label"
|
|
>Quantité Conditionnement</label
|
|
>
|
|
<soft-input
|
|
id="conditionnement_quantite"
|
|
v-model.number="form.conditionnement_quantite"
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
:class="{
|
|
'is-invalid': validationErrors.conditionnement_quantite,
|
|
}"
|
|
placeholder="0.00"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.conditionnement_quantite"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.conditionnement_quantite[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label for="conditionnement_unite" class="form-label"
|
|
>Unité Conditionnement</label
|
|
>
|
|
<soft-input
|
|
id="conditionnement_unite"
|
|
v-model="form.conditionnement_unite"
|
|
type="text"
|
|
:class="{
|
|
'is-invalid': validationErrors.conditionnement_unite,
|
|
}"
|
|
placeholder="Ex: pièce, kg..."
|
|
/>
|
|
<div
|
|
v-if="validationErrors.conditionnement_unite"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.conditionnement_unite[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URLs -->
|
|
<div v-if="!isIntervention" class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label for="photo_url" class="form-label"
|
|
>URL de la Photo</label
|
|
>
|
|
<soft-input
|
|
id="photo_url"
|
|
v-model="form.photo_url"
|
|
type="url"
|
|
:class="{ 'is-invalid': validationErrors.photo_url }"
|
|
placeholder="https://exemple.com/photo.jpg"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.photo_url"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.photo_url[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label for="fiche_technique_url" class="form-label"
|
|
>URL Fiche Technique</label
|
|
>
|
|
<soft-input
|
|
id="fiche_technique_url"
|
|
v-model="form.fiche_technique_url"
|
|
type="url"
|
|
:class="{
|
|
'is-invalid': validationErrors.fiche_technique_url,
|
|
}"
|
|
placeholder="https://exemple.com/fiche.pdf"
|
|
/>
|
|
<div
|
|
v-if="validationErrors.fiche_technique_url"
|
|
class="invalid-feedback"
|
|
>
|
|
{{ validationErrors.fiche_technique_url[0] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="row mt-4">
|
|
<div class="col-12 d-flex justify-content-end">
|
|
<soft-button
|
|
type="soft-button"
|
|
class="btn btn-light me-3"
|
|
@click="$router.go(-1)"
|
|
>
|
|
Annuler
|
|
</soft-button>
|
|
<soft-button
|
|
type="submit"
|
|
class="btn bg-gradient-success"
|
|
:disabled="loading"
|
|
>
|
|
<span
|
|
v-if="loading"
|
|
class="spinner-border spinner-border-sm me-2"
|
|
role="status"
|
|
></span>
|
|
Créer le Produit
|
|
</soft-button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import SoftInput from "@/components/SoftInput.vue";
|
|
import SoftButton from "@/components/SoftButton.vue";
|
|
import { ref, reactive, defineProps, defineEmits } from "vue";
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
fournisseurs: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
validationErrors: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
categories: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
success: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
// Emits
|
|
const emit = defineEmits(["create-product"]);
|
|
|
|
// Form data
|
|
const form = reactive({
|
|
nom: "",
|
|
reference: "",
|
|
categorie_id: "",
|
|
fabricant: "",
|
|
stock_actuel: 0,
|
|
stock_minimum: 0,
|
|
unite: "",
|
|
prix_unitaire: 0,
|
|
date_expiration: "",
|
|
numero_lot: "",
|
|
conditionnement_nom: "",
|
|
conditionnement_quantite: 0,
|
|
conditionnement_unite: "",
|
|
photo_url: "",
|
|
fiche_technique_url: "",
|
|
fournisseur_id: "",
|
|
});
|
|
|
|
// Computed property to check if selected category is an intervention
|
|
import { computed, watch } from "vue";
|
|
|
|
const isIntervention = computed(() => {
|
|
const selectedCategory = props.categories.find(
|
|
(c) => c.id === form.categorie_id
|
|
);
|
|
return selectedCategory ? selectedCategory.intervention : false;
|
|
});
|
|
|
|
// Watch logic for intervention categories
|
|
watch(
|
|
() => form.categorie_id,
|
|
(newVal) => {
|
|
if (isIntervention.value) {
|
|
form.stock_actuel = 1;
|
|
form.stock_minimum = 1;
|
|
form.unite = "pièce";
|
|
// Clear or set defaults for hidden fields if needed
|
|
form.fabricant = "";
|
|
form.date_expiration = "";
|
|
form.numero_lot = "";
|
|
form.conditionnement_nom = "";
|
|
form.conditionnement_quantite = 0;
|
|
form.conditionnement_unite = "";
|
|
form.photo_url = "";
|
|
form.fiche_technique_url = "";
|
|
form.fournisseur_id = "";
|
|
}
|
|
}
|
|
);
|
|
|
|
// Auto-generate reference for intervention products
|
|
watch(
|
|
() => form.nom,
|
|
(newVal) => {
|
|
if (isIntervention.value && newVal) {
|
|
form.reference = generateReference(newVal);
|
|
}
|
|
}
|
|
);
|
|
|
|
const generateReference = (name) => {
|
|
const prefix = name.substring(0, 3).toUpperCase();
|
|
const randomSuffix = Math.floor(Math.random() * 10000)
|
|
.toString()
|
|
.padStart(4, "0");
|
|
return `${prefix}-${randomSuffix}`;
|
|
};
|
|
|
|
// Methods
|
|
const handleSubmit = () => {
|
|
// Clean up the form data
|
|
const formData = { ...form };
|
|
|
|
// Convert empty strings to null for optional fields
|
|
Object.keys(formData).forEach((key) => {
|
|
if (formData[key] === "") {
|
|
formData[key] = null;
|
|
}
|
|
});
|
|
|
|
emit("create-product", formData);
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.invalid-feedback {
|
|
display: block;
|
|
}
|
|
|
|
.soft-input-group-text {
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.card {
|
|
border: none;
|
|
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
|
|
}
|
|
|
|
.form-control:focus {
|
|
border-color: #80bdff;
|
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
|
}
|
|
|
|
.alert {
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
}
|
|
</style>
|