add employee
This commit is contained in:
parent
aa306f5d19
commit
0ea8f1866b
@ -0,0 +1,310 @@
|
|||||||
|
<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 de l'Employé</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Success Message -->
|
||||||
|
<div v-if="success" class="alert alert-success" role="alert">
|
||||||
|
<strong>Succès!</strong> L'employé 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 de l'employé...</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="first_name" class="form-label">Prénom *</label>
|
||||||
|
<soft-input
|
||||||
|
id="first_name"
|
||||||
|
v-model="form.first_name"
|
||||||
|
type="text"
|
||||||
|
:class="{ 'is-invalid': validationErrors.first_name }"
|
||||||
|
required
|
||||||
|
placeholder="Entrez le prénom"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="validationErrors.first_name"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.first_name[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="last_name" class="form-label">Nom *</label>
|
||||||
|
<soft-input
|
||||||
|
id="last_name"
|
||||||
|
v-model="form.last_name"
|
||||||
|
type="text"
|
||||||
|
:class="{ 'is-invalid': validationErrors.last_name }"
|
||||||
|
required
|
||||||
|
placeholder="Entrez le nom"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="validationErrors.last_name"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.last_name[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="form-label">Email</label>
|
||||||
|
<soft-input
|
||||||
|
id="email"
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
:class="{ 'is-invalid': validationErrors.email }"
|
||||||
|
placeholder="entreprise@exemple.com"
|
||||||
|
/>
|
||||||
|
<div v-if="validationErrors.email" class="invalid-feedback">
|
||||||
|
{{ validationErrors.email[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone" class="form-label">Téléphone</label>
|
||||||
|
<soft-input
|
||||||
|
id="phone"
|
||||||
|
v-model="form.phone"
|
||||||
|
type="tel"
|
||||||
|
:class="{ 'is-invalid': validationErrors.phone }"
|
||||||
|
placeholder="06 12 34 56 78"
|
||||||
|
/>
|
||||||
|
<div v-if="validationErrors.phone" class="invalid-feedback">
|
||||||
|
{{ validationErrors.phone[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hire_date" class="form-label"
|
||||||
|
>Date d'Embauche *</label
|
||||||
|
>
|
||||||
|
<soft-input
|
||||||
|
id="hire_date"
|
||||||
|
v-model="form.hire_date"
|
||||||
|
type="date"
|
||||||
|
:class="{ 'is-invalid': validationErrors.hire_date }"
|
||||||
|
required
|
||||||
|
:max="new Date().toISOString().split('T')[0]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="validationErrors.hire_date"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.hire_date[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="job_title" class="form-label">Poste</label>
|
||||||
|
<soft-input
|
||||||
|
id="job_title"
|
||||||
|
v-model="form.job_title"
|
||||||
|
type="text"
|
||||||
|
:class="{ 'is-invalid': validationErrors.job_title }"
|
||||||
|
placeholder="Entrez le poste occupé"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="validationErrors.job_title"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.job_title[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Salary and Status -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="salary" class="form-label">Salaire (€)</label>
|
||||||
|
<soft-input
|
||||||
|
id="salary"
|
||||||
|
v-model.number="form.salary"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
:class="{ 'is-invalid': validationErrors.salary }"
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="validationErrors.salary"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.salary[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="active" class="form-label">Statut</label>
|
||||||
|
<select
|
||||||
|
id="active"
|
||||||
|
v-model="form.active"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': validationErrors.active }"
|
||||||
|
>
|
||||||
|
<option value="true">Actif</option>
|
||||||
|
<option value="false">Inactif</option>
|
||||||
|
</select>
|
||||||
|
<div
|
||||||
|
v-if="validationErrors.active"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.active[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 l'Employé
|
||||||
|
</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({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validationErrors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(["createEmployee"]);
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
const form = reactive({
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
hire_date: new Date().toISOString().split("T")[0], // Default to today
|
||||||
|
job_title: "",
|
||||||
|
salary: 0,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert active string to boolean
|
||||||
|
if (typeof formData.active === "string") {
|
||||||
|
formData.active = formData.active === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("createEmployee", formData);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<employee-template>
|
||||||
|
<template #employee-new-action>
|
||||||
|
<add-button text="Ajouter" @click="goToEmployee" />
|
||||||
|
</template>
|
||||||
|
<template #select-filter>
|
||||||
|
<filter-table />
|
||||||
|
</template>
|
||||||
|
<template #employee-other-action>
|
||||||
|
<table-action />
|
||||||
|
</template>
|
||||||
|
<template #employee-table>
|
||||||
|
<employee-table
|
||||||
|
:data="employeeData"
|
||||||
|
:loading="loadingData"
|
||||||
|
@view="goToDetails"
|
||||||
|
@delete="deleteEmployee"
|
||||||
|
/>
|
||||||
|
<!-- Pagination Component -->
|
||||||
|
<div v-if="pagination && pagination.total > 0" class="mt-4 px-4">
|
||||||
|
<nav aria-label="Employee pagination">
|
||||||
|
<ul class="pagination justify-content-center mb-0">
|
||||||
|
<li
|
||||||
|
class="page-item"
|
||||||
|
:class="{ disabled: pagination.current_page === 1 }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="page-link"
|
||||||
|
@click="changePage(pagination.current_page - 1)"
|
||||||
|
:disabled="pagination.current_page === 1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
v-for="page in visiblePages"
|
||||||
|
:key="page"
|
||||||
|
class="page-item"
|
||||||
|
:class="{ active: page === pagination.current_page }"
|
||||||
|
>
|
||||||
|
<button class="page-link" @click="changePage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
class="page-item"
|
||||||
|
:class="{
|
||||||
|
disabled: pagination.current_page === pagination.last_page,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="page-link"
|
||||||
|
@click="changePage(pagination.current_page + 1)"
|
||||||
|
:disabled="pagination.current_page === pagination.last_page"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Pagination Info -->
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<small class="text-muted">
|
||||||
|
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||||
|
{{ pagination.total }} employés
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</employee-template>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import EmployeeTemplate from "@/components/templates/CRM/EmployeeTemplate.vue";
|
||||||
|
import EmployeeTable from "@/components/molecules/Employees/EmployeeTable.vue";
|
||||||
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const emit = defineEmits(["pushDetails", "deleteEmployee"]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
employeeData: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loadingData: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToEmployee = () => {
|
||||||
|
router.push({
|
||||||
|
name: "Creation employé",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToDetails = (employeeId) => {
|
||||||
|
emit("pushDetails", employeeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEmployee = (employeeId) => {
|
||||||
|
emit("deleteEmployee", employeeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computed property for visible page numbers
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
if (!props.pagination) return [];
|
||||||
|
|
||||||
|
const current = props.pagination.current_page;
|
||||||
|
const last = props.pagination.last_page;
|
||||||
|
const delta = 2; // Number of pages to show on each side of current page
|
||||||
|
|
||||||
|
const range = [];
|
||||||
|
const rangeWithDots = [];
|
||||||
|
|
||||||
|
// Calculate range around current page
|
||||||
|
for (
|
||||||
|
let i = Math.max(2, current - delta);
|
||||||
|
i <= Math.min(last - 1, current + delta);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
range.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add first page
|
||||||
|
if (current - delta > 2) {
|
||||||
|
rangeWithDots.push(1, "...");
|
||||||
|
} else {
|
||||||
|
rangeWithDots.push(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add calculated range
|
||||||
|
rangeWithDots.push(...range);
|
||||||
|
|
||||||
|
// Add last page
|
||||||
|
if (current + delta < last - 1) {
|
||||||
|
rangeWithDots.push("...", last);
|
||||||
|
} else if (last > 1) {
|
||||||
|
rangeWithDots.push(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rangeWithDots;
|
||||||
|
});
|
||||||
|
|
||||||
|
const changePage = (page) => {
|
||||||
|
if (page >= 1 && page <= props.pagination.last_page) {
|
||||||
|
// Emit event for parent component to handle page change
|
||||||
|
emit("changePage", page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagination {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
color: #5e72e4;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link:hover {
|
||||||
|
color: #5e72e4;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
background-color: #5e72e4;
|
||||||
|
border-color: #5e72e4;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
color: #6c757d;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:first-child .page-link {
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:last-child .page-link {
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<product-category-template>
|
||||||
|
<template #product-category-new-action>
|
||||||
|
<add-button text="Ajouter" @click="openAddModal" />
|
||||||
|
</template>
|
||||||
|
<template #select-filter>
|
||||||
|
<filter-table />
|
||||||
|
</template>
|
||||||
|
<template #product-category-other-action>
|
||||||
|
<table-action />
|
||||||
|
</template>
|
||||||
|
<template #product-category-table>
|
||||||
|
<product-category-table
|
||||||
|
:data="categoryData"
|
||||||
|
:loading="loadingData"
|
||||||
|
@view="goToDetails"
|
||||||
|
@edit="openEditModal"
|
||||||
|
@delete="deleteCategory"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</product-category-template>
|
||||||
|
|
||||||
|
<!-- Add/Edit Product Category Modal -->
|
||||||
|
<product-category-modal
|
||||||
|
:is-visible="modalIsVisible"
|
||||||
|
:is-modification="isModification"
|
||||||
|
:category="selectedCategory"
|
||||||
|
:parent-categories="parentCategories"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
@close="closeModal"
|
||||||
|
@category-created="handleCategoryCreated"
|
||||||
|
@category-modified="handleCategoryModified"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import ProductCategoryTemplate from "@/components/templates/Stock/ProductCategoryTemplate.vue";
|
||||||
|
import ProductCategoryTable from "@/components/molecules/Tables/Stock/ProductCategoryTable.vue";
|
||||||
|
import ProductCategoryModal from "@/components/molecules/Stock/ProductCategoryModal.vue";
|
||||||
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { defineProps, defineEmits, ref, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
const emit = defineEmits(["pushDetails", "deleteCategory"]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
categoryData: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loadingData: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const modalIsVisible = ref(false);
|
||||||
|
const isModification = ref(false);
|
||||||
|
const selectedCategory = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
// Get parent categories for dropdown (excluding the current category when editing)
|
||||||
|
const parentCategories = computed(() => {
|
||||||
|
if (isModification.value && selectedCategory.value) {
|
||||||
|
return props.categoryData.filter(
|
||||||
|
(category) => category.id !== selectedCategory.value.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return props.categoryData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal handlers
|
||||||
|
const openAddModal = () => {
|
||||||
|
isModification.value = false;
|
||||||
|
selectedCategory.value = null;
|
||||||
|
modalIsVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (category) => {
|
||||||
|
if (typeof category === "string") {
|
||||||
|
// Find category by ID
|
||||||
|
const foundCategory = props.categoryData.find(
|
||||||
|
(cat) => cat.id.toString() === category
|
||||||
|
);
|
||||||
|
if (foundCategory) {
|
||||||
|
selectedCategory.value = foundCategory;
|
||||||
|
} else {
|
||||||
|
notificationStore.error("Erreur", "Catégorie non trouvée");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedCategory.value = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
isModification.value = true;
|
||||||
|
modalIsVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
modalIsVisible.value = false;
|
||||||
|
isModification.value = false;
|
||||||
|
selectedCategory.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCategoryCreated = async (formData) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
await productCategoryStore.createProductCategory(formData);
|
||||||
|
notificationStore.created("Catégorie de produit");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating category:", error);
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
error.response.data.message || "Erreur lors de la création"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Erreur lors de la création de la catégorie"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCategoryModified = async (formData) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
await productCategoryStore.updateProductCategory(formData);
|
||||||
|
notificationStore.updated("Catégorie de produit");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating category:", error);
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
error.response.data.message || "Erreur lors de la modification"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Erreur lors de la modification de la catégorie"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Original handlers
|
||||||
|
const goToDetails = (category) => {
|
||||||
|
if (typeof category === "string") {
|
||||||
|
emit("pushDetails", category);
|
||||||
|
} else {
|
||||||
|
emit("pushDetails", category.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCategory = (category) => {
|
||||||
|
if (typeof category === "string") {
|
||||||
|
emit("deleteCategory", category);
|
||||||
|
} else {
|
||||||
|
emit("deleteCategory", category.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,507 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-container">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-content">
|
||||||
|
<!-- Skeleton Rows -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nom & Prénom</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Téléphone</th>
|
||||||
|
<th>Poste</th>
|
||||||
|
<th>Date d'embauche</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
|
||||||
|
<!-- ID Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-checkbox"></div>
|
||||||
|
<div class="skeleton-text short ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Name Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-text medium ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Email Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text long"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Phone Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text medium"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Position Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-icon"></div>
|
||||||
|
<div class="skeleton-text medium ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Hire Date Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text medium"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
<div class="skeleton-text short ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data State -->
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table id="employee-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nom & Prénom</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Téléphone</th>
|
||||||
|
<th>Poste</th>
|
||||||
|
<th>Date d'embauche</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="employee in data" :key="employee.id">
|
||||||
|
<!-- ID Column -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-checkbox />
|
||||||
|
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||||
|
{{ employee.id }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-avatar
|
||||||
|
:img="getRandomAvatar()"
|
||||||
|
size="xs"
|
||||||
|
class="me-2"
|
||||||
|
alt="user image"
|
||||||
|
circular
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
>{{ employee.last_name }} {{ employee.first_name }}</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="employee.thanatopractitioner"
|
||||||
|
class="text-xs text-info"
|
||||||
|
>
|
||||||
|
Thanatopractitioner
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Email Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="text-xs">{{ employee.email || "N/A" }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Phone Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Position Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-button
|
||||||
|
:color="getPositionColor(employee.job_title)"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="getPositionIcon(employee.job_title)"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</soft-button>
|
||||||
|
<span>{{ employee.job_title || "N/A" }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Hire Date Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="text-xs">{{ formatDate(employee.hire_date) }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-button
|
||||||
|
:color="employee.active ? 'success' : 'danger'"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</soft-button>
|
||||||
|
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<!-- View Button -->
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
title="Voir l'employé"
|
||||||
|
:data-employee-id="employee.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<soft-button
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
title="Supprimer l'employé"
|
||||||
|
:data-employee-id="employee.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-users fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="empty-title">Aucun employé trouvé</h5>
|
||||||
|
<p class="empty-text text-muted">
|
||||||
|
Aucun employé à afficher pour le moment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||||
|
import { DataTable } from "simple-datatables";
|
||||||
|
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "delete"]);
|
||||||
|
|
||||||
|
// Sample avatar images
|
||||||
|
import img1 from "@/assets/img/team-2.jpg";
|
||||||
|
import img2 from "@/assets/img/team-1.jpg";
|
||||||
|
import img3 from "@/assets/img/team-3.jpg";
|
||||||
|
import img4 from "@/assets/img/team-4.jpg";
|
||||||
|
import img5 from "@/assets/img/team-5.jpg";
|
||||||
|
import img6 from "@/assets/img/ivana-squares.jpg";
|
||||||
|
|
||||||
|
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
skeletonRows: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getRandomAvatar = () => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * avatarImages.length);
|
||||||
|
return avatarImages[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return "N/A";
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("fr-FR");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPositionColor = (position) => {
|
||||||
|
const colors = {
|
||||||
|
Manager: "info",
|
||||||
|
Technician: "success",
|
||||||
|
Supervisor: "warning",
|
||||||
|
Assistant: "secondary",
|
||||||
|
Director: "danger",
|
||||||
|
};
|
||||||
|
return colors[position] || "secondary";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPositionIcon = (position) => {
|
||||||
|
const icons = {
|
||||||
|
Manager: "fas fa-user-tie",
|
||||||
|
Technician: "fas fa-tools",
|
||||||
|
Supervisor: "fas fa-user-cog",
|
||||||
|
Assistant: "fas fa-user",
|
||||||
|
Director: "fas fa-crown",
|
||||||
|
};
|
||||||
|
return icons[position] || "fas fa-user";
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeDataTable = () => {
|
||||||
|
// Destroy existing instance if it exists
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTableEl = document.getElementById("employee-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
|
searchable: true,
|
||||||
|
fixedHeight: true,
|
||||||
|
perPage: 15,
|
||||||
|
perPageSelect: [5, 10, 15, 20, 25],
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableClick = (event) => {
|
||||||
|
const button = event.target.closest("button");
|
||||||
|
if (!button) return;
|
||||||
|
const employeeId = button.getAttribute("data-employee-id");
|
||||||
|
if (
|
||||||
|
button.title === "Supprimer l'employé" ||
|
||||||
|
button.querySelector(".fa-trash")
|
||||||
|
) {
|
||||||
|
emit("delete", employeeId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Voir l'employé" ||
|
||||||
|
button.querySelector(".fa-eye")
|
||||||
|
) {
|
||||||
|
emit("view", employeeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for data changes to reinitialize datatable
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!props.loading) {
|
||||||
|
// Small delay to ensure DOM is updated
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const dataTableEl = document.getElementById("employee-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize data
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.loading && props.data.length > 0) {
|
||||||
|
initializeDataTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row {
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon.small {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.short {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.loading-spinner {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,473 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Modal Component -->
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
:class="{ show: isVisible, 'd-block': isVisible }"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
:aria-hidden="!isVisible"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i
|
||||||
|
:class="isModification ? 'fas fa-edit me-2' : 'fas fa-tags me-2'"
|
||||||
|
></i>
|
||||||
|
{{
|
||||||
|
isModification ? "Modifier la catégorie" : "Ajouter une catégorie"
|
||||||
|
}}
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
aria-label="Close"
|
||||||
|
@click="closeModal"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<!-- Code -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
>Code <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="formData.code"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.code }"
|
||||||
|
placeholder="Code de la catégorie"
|
||||||
|
maxlength="50"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="errors.code" class="invalid-feedback">
|
||||||
|
{{ errors.code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
>Nom <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="formData.name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.name }"
|
||||||
|
placeholder="Nom de la catégorie"
|
||||||
|
maxlength="191"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="errors.name" class="invalid-feedback">
|
||||||
|
{{ errors.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Parent Category -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Catégorie parent</label>
|
||||||
|
<select
|
||||||
|
v-model="formData.parent_id"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.parent_id }"
|
||||||
|
>
|
||||||
|
<option value="">Catégorie racine</option>
|
||||||
|
<option
|
||||||
|
v-for="category in parentCategories"
|
||||||
|
:key="category.id"
|
||||||
|
:value="category.id"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div v-if="errors.parent_id" class="invalid-feedback">
|
||||||
|
{{ errors.parent_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.description }"
|
||||||
|
placeholder="Description de la catégorie"
|
||||||
|
rows="3"
|
||||||
|
maxlength="500"
|
||||||
|
></textarea>
|
||||||
|
<div v-if="errors.description" class="invalid-feedback">
|
||||||
|
{{ errors.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Status -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
v-model="formData.active"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="activeSwitch"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="activeSwitch">
|
||||||
|
Catégorie active
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="errors.active" class="invalid-feedback d-block">
|
||||||
|
{{ errors.active }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- General Error -->
|
||||||
|
<div v-if="errors.general" class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
{{ errors.general }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times me-1"></i>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="submitForm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save me-1"></i>
|
||||||
|
{{
|
||||||
|
isLoading
|
||||||
|
? isModification
|
||||||
|
? "Modification..."
|
||||||
|
: "Création..."
|
||||||
|
: isModification
|
||||||
|
? "Modifier la catégorie"
|
||||||
|
: "Créer la catégorie"
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
class="modal-backdrop fade show"
|
||||||
|
@click="closeModal"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch } from "vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isModification: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
parentCategories: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(["close", "category-created", "category-modified"]);
|
||||||
|
|
||||||
|
// Store
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const errors = reactive({});
|
||||||
|
const formData = reactive({
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
parent_id: "",
|
||||||
|
description: "",
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for category changes (for modification mode)
|
||||||
|
watch(
|
||||||
|
() => props.category,
|
||||||
|
(newCategory) => {
|
||||||
|
if (newCategory && props.isModification) {
|
||||||
|
formData.code = newCategory.code || "";
|
||||||
|
formData.name = newCategory.name || "";
|
||||||
|
formData.parent_id = newCategory.parent_id || "";
|
||||||
|
formData.description = newCategory.description || "";
|
||||||
|
formData.active =
|
||||||
|
newCategory.active !== undefined ? newCategory.active : true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset form when modal closes
|
||||||
|
watch(
|
||||||
|
() => props.isVisible,
|
||||||
|
(isVisible) => {
|
||||||
|
if (!isVisible) {
|
||||||
|
setTimeout(resetForm, 150); // Wait for modal animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const closeModal = () => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.code = "";
|
||||||
|
formData.name = "";
|
||||||
|
formData.parent_id = "";
|
||||||
|
formData.description = "";
|
||||||
|
formData.active = true;
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
// Clear previous errors
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Code validation
|
||||||
|
if (!formData.code || formData.code.trim() === "") {
|
||||||
|
errors.code = "Le code est obligatoire.";
|
||||||
|
isValid = false;
|
||||||
|
} else if (formData.code.length > 50) {
|
||||||
|
errors.code = "Le code ne peut pas dépasser 50 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name validation
|
||||||
|
if (!formData.name || formData.name.trim() === "") {
|
||||||
|
errors.name = "Le nom est obligatoire.";
|
||||||
|
isValid = false;
|
||||||
|
} else if (formData.name.length > 191) {
|
||||||
|
errors.name = "Le nom ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description validation
|
||||||
|
if (formData.description && formData.description.length > 500) {
|
||||||
|
errors.description = "La description ne peut pas dépasser 500 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent category validation
|
||||||
|
if (formData.parent_id && formData.parent_id === props.category?.id) {
|
||||||
|
errors.parent_id = "Une catégorie ne peut pas être sa propre parente.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate code in parent categories (excluding current category for edit)
|
||||||
|
const existingCategory = props.parentCategories.find(
|
||||||
|
(cat) => cat.code === formData.code && cat.id !== props.category?.id
|
||||||
|
);
|
||||||
|
if (existingCategory) {
|
||||||
|
errors.code = "Ce code de catégorie existe déjà.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare data for API
|
||||||
|
const submitData = { ...formData };
|
||||||
|
|
||||||
|
// Convert empty strings to null for nullable fields
|
||||||
|
if (submitData.parent_id === "") {
|
||||||
|
submitData.parent_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit event - parent will handle the API call and loading state
|
||||||
|
if (props.isModification) {
|
||||||
|
submitData.id = props.category.id;
|
||||||
|
emit("category-modified", submitData);
|
||||||
|
} else {
|
||||||
|
emit("category-created", submitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reset form
|
||||||
|
closeModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la sauvegarde de la catégorie:", error);
|
||||||
|
|
||||||
|
// Handle API errors
|
||||||
|
if (error.response && error.response.data && error.response.data.errors) {
|
||||||
|
Object.assign(errors, error.response.data.errors);
|
||||||
|
} else {
|
||||||
|
errors.general = "Une erreur est survenue lors de la sauvegarde.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard event listener for ESC key
|
||||||
|
const handleKeydown = (event) => {
|
||||||
|
if (event.key === "Escape" && props.isVisible) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener when component is mounted
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label .text-danger {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border: 1px solid #dce1e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(203, 12, 159, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
border: 1px solid #dce1e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(203, 12, 159, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #cb0c9f;
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: #a90982;
|
||||||
|
border-color: #a90982;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade {
|
||||||
|
transition: opacity 0.15s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: #cb0c9f;
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:focus {
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(203, 12, 159, 0.25);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,516 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-container">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-content">
|
||||||
|
<!-- Skeleton Rows -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Catégorie parent</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Produits</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
|
||||||
|
<!-- Code Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-text medium ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Name Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-text long ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Parent Category Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text medium"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Description Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text very-long"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-icon"></div>
|
||||||
|
<div class="skeleton-text short ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Products Count Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text short"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data State -->
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table id="product-category-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Catégorie parent</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Produits</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="category in data" :key="category.id">
|
||||||
|
<!-- Code Column -->
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span>{{ category.code }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-weight-bold">
|
||||||
|
{{ category.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted">ID: {{ category.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Parent Category Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div v-if="category.parent" class="d-flex align-items-center">
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<span>{{ category.parent.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-muted">
|
||||||
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder-open" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
Catégorie racine
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Description Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="description-cell">
|
||||||
|
<span
|
||||||
|
v-if="category.description"
|
||||||
|
class="text-truncate d-inline-block"
|
||||||
|
style="max-width: 200px"
|
||||||
|
>
|
||||||
|
{{ category.description }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-muted">Aucune description</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<span :class="getStatusClass(category)">
|
||||||
|
<i
|
||||||
|
:class="category.active ? 'fas fa-check me-1' : 'fas fa-times me-1'"
|
||||||
|
></i>
|
||||||
|
{{ category.active ? "Active" : "Inactive" }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Children Badge -->
|
||||||
|
<span
|
||||||
|
v-if="category.has_children"
|
||||||
|
class="badge badge-info mt-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sitemap me-1"></i>
|
||||||
|
{{ category.children_count || 0 }} enfants
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Products Count Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-badge
|
||||||
|
:color="category.has_products ? 'success' : 'secondary'"
|
||||||
|
class="me-2"
|
||||||
|
>
|
||||||
|
{{ category.products_count || 0 }}
|
||||||
|
</soft-badge>
|
||||||
|
<span class="text-muted">produits</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<!-- View Button -->
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
title="Voir la catégorie"
|
||||||
|
:data-category-id="category.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('view', category)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<soft-button
|
||||||
|
color="warning"
|
||||||
|
variant="outline"
|
||||||
|
title="Modifier la catégorie"
|
||||||
|
:data-category-id="category.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('edit', category)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<soft-button
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
title="Supprimer la catégorie"
|
||||||
|
:data-category-id="category.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('delete', category)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-tags fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="empty-title">Aucune catégorie de produit trouvée</h5>
|
||||||
|
<p class="empty-text text-muted">
|
||||||
|
Aucune catégorie de produit à afficher pour le moment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||||
|
import { DataTable } from "simple-datatables";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||||
|
import SoftBadge from "@/components/SoftBadge.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "edit", "delete"]);
|
||||||
|
|
||||||
|
// Sample avatar images for categories
|
||||||
|
import img1 from "@/assets/img/team-2.jpg";
|
||||||
|
import img2 from "@/assets/img/team-1.jpg";
|
||||||
|
import img3 from "@/assets/img/team-3.jpg";
|
||||||
|
import img4 from "@/assets/img/team-4.jpg";
|
||||||
|
import img5 from "@/assets/img/team-5.jpg";
|
||||||
|
import img6 from "@/assets/img/ivana-squares.jpg";
|
||||||
|
|
||||||
|
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
skeletonRows: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getRandomAvatar = () => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * avatarImages.length);
|
||||||
|
return avatarImages[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (category) => {
|
||||||
|
return category.active ? "badge badge-success" : "badge badge-danger";
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeDataTable = () => {
|
||||||
|
// Destroy existing instance if it exists
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTableEl = document.getElementById("product-category-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
|
searchable: true,
|
||||||
|
fixedHeight: true,
|
||||||
|
perPage: 10,
|
||||||
|
perPageSelect: [5, 10, 15, 20],
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableClick = (event) => {
|
||||||
|
const button = event.target.closest("button");
|
||||||
|
if (!button) return;
|
||||||
|
const categoryId = button.getAttribute("data-category-id");
|
||||||
|
|
||||||
|
if (
|
||||||
|
button.title === "Supprimer la catégorie" ||
|
||||||
|
button.querySelector(".fa-trash")
|
||||||
|
) {
|
||||||
|
emit("delete", categoryId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Modifier la catégorie" ||
|
||||||
|
button.querySelector(".fa-edit")
|
||||||
|
) {
|
||||||
|
emit("edit", categoryId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Voir la catégorie" ||
|
||||||
|
button.querySelector(".fa-eye")
|
||||||
|
) {
|
||||||
|
emit("view", categoryId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for data changes to reinitialize datatable
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!props.loading) {
|
||||||
|
// Small delay to ensure DOM is updated
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const dataTableEl = document.getElementById("product-category-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize data
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.loading && props.data.length > 0) {
|
||||||
|
initializeDataTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row {
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.short {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.very-long {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-cell {
|
||||||
|
max-width: 200px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.loading-spinner {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.very-long {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon.small {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,532 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-container">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-content">
|
||||||
|
<!-- Skeleton Rows -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Catégorie parent</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Produits</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
|
||||||
|
<!-- Code Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-icon"></div>
|
||||||
|
<div class="skeleton-text medium ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Name Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-text long ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Parent Category Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text medium"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Description Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text very-long"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-icon"></div>
|
||||||
|
<div class="skeleton-text short ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Products Count Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text short"></div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Action Column Skeleton -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
<div class="skeleton-icon small"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data State -->
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table id="product-category-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Catégorie parent</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Produits</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="category in data" :key="category.id">
|
||||||
|
<!-- Code Column -->
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-button
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-tag" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<span>{{ category.code }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-avatar
|
||||||
|
:img="getRandomAvatar()"
|
||||||
|
size="xs"
|
||||||
|
class="me-2"
|
||||||
|
alt="category image"
|
||||||
|
circular
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-weight-bold">
|
||||||
|
{{ category.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted">ID: {{ category.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Parent Category Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div v-if="category.parent" class="d-flex align-items-center">
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<span>{{ category.parent.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-muted">
|
||||||
|
<i class="fas fa-folder-open me-1"></i>
|
||||||
|
Catégorie racine
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Description Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="description-cell">
|
||||||
|
<span
|
||||||
|
v-if="category.description"
|
||||||
|
class="text-truncate d-inline-block"
|
||||||
|
style="max-width: 200px"
|
||||||
|
>
|
||||||
|
{{ category.description }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-muted">Aucune description</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<!-- Has Children Badge -->
|
||||||
|
<soft-button
|
||||||
|
v-if="category.has_children"
|
||||||
|
color="warning"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-sm mb-1"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sitemap me-1"></i>
|
||||||
|
Parent
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Has Products Badge -->
|
||||||
|
<soft-button
|
||||||
|
v-if="category.has_products"
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-sm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-boxes me-1"></i>
|
||||||
|
Utilisée
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Normal Status -->
|
||||||
|
<span
|
||||||
|
v-if="!category.has_children && !category.has_products"
|
||||||
|
class="badge badge-success"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check me-1"></i>
|
||||||
|
Libre
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Products Count Column -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="products-info">
|
||||||
|
<div>{{ category.products_count || 0 }}</div>
|
||||||
|
<div class="text-xs text-muted">produits</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<!-- View Button -->
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
title="Voir la catégorie"
|
||||||
|
:data-category-id="category.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('view', category)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<soft-button
|
||||||
|
color="warning"
|
||||||
|
variant="outline"
|
||||||
|
title="Modifier la catégorie"
|
||||||
|
:data-category-id="category.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('edit', category)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<soft-button
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
title="Supprimer la catégorie"
|
||||||
|
:data-category-id="category.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('delete', category)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-tags fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="empty-title">Aucune catégorie de produit trouvée</h5>
|
||||||
|
<p class="empty-text text-muted">
|
||||||
|
Aucune catégorie de produit à afficher pour le moment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||||
|
import { DataTable } from "simple-datatables";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
// Sample avatar images for categories
|
||||||
|
import img1 from "@/assets/img/team-2.jpg";
|
||||||
|
import img2 from "@/assets/img/team-1.jpg";
|
||||||
|
import img3 from "@/assets/img/team-3.jpg";
|
||||||
|
import img4 from "@/assets/img/team-4.jpg";
|
||||||
|
import img5 from "@/assets/img/team-5.jpg";
|
||||||
|
import img6 from "@/assets/img/ivana-squares.jpg";
|
||||||
|
|
||||||
|
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "edit", "delete"]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
skeletonRows: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getRandomAvatar = () => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * avatarImages.length);
|
||||||
|
return avatarImages[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeDataTable = () => {
|
||||||
|
// Destroy existing instance if it exists
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTableEl = document.getElementById("product-category-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
|
searchable: true,
|
||||||
|
fixedHeight: true,
|
||||||
|
perPage: 10,
|
||||||
|
perPageSelect: [5, 10, 15, 20],
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableClick = (event) => {
|
||||||
|
const button = event.target.closest("button");
|
||||||
|
if (!button) return;
|
||||||
|
const categoryId = button.getAttribute("data-category-id");
|
||||||
|
|
||||||
|
if (
|
||||||
|
button.title === "Supprimer la catégorie" ||
|
||||||
|
button.querySelector(".fa-trash")
|
||||||
|
) {
|
||||||
|
emit("delete", categoryId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Modifier la catégorie" ||
|
||||||
|
button.querySelector(".fa-edit")
|
||||||
|
) {
|
||||||
|
emit("edit", categoryId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Voir la catégorie" ||
|
||||||
|
button.querySelector(".fa-eye")
|
||||||
|
) {
|
||||||
|
emit("view", categoryId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for data changes to reinitialize datatable
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!props.loading) {
|
||||||
|
// Small delay to ensure DOM is updated
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const dataTableEl = document.getElementById("product-category-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize data
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.loading && props.data.length > 0) {
|
||||||
|
initializeDataTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row {
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.short {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.very-long {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-cell {
|
||||||
|
max-width: 200px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-info {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.loading-spinner {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.very-long {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon.small {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
||||||
|
<h5 class="font-weight-bolder mb-0">Nouvel Employé</h5>
|
||||||
|
<p class="mb-0 text-sm">Informations de l'employé</p>
|
||||||
|
|
||||||
|
<div class="multisteps-form__content">
|
||||||
|
<!-- Nom & Prénom -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label"
|
||||||
|
>Prénom <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<soft-input
|
||||||
|
:value="form.first_name"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.first_name }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Jean"
|
||||||
|
maxlength="191"
|
||||||
|
@input="form.first_name = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.first_name" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.first_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label"
|
||||||
|
>Nom de famille <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<soft-input
|
||||||
|
:value="form.last_name"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.last_name }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Dupont"
|
||||||
|
maxlength="191"
|
||||||
|
@input="form.last_name = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.last_name" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.last_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email & Téléphone -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.email"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.email }"
|
||||||
|
type="email"
|
||||||
|
placeholder="ex. jean.dupont@entreprise.com"
|
||||||
|
maxlength="191"
|
||||||
|
@input="form.email = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.email" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Téléphone</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.phone"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.phone }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. +261341234567"
|
||||||
|
maxlength="50"
|
||||||
|
@input="form.phone = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.phone" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.phone }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Intitulé du poste -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Intitulé du poste</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.job_title"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.job_title }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Développeur Full-Stack"
|
||||||
|
maxlength="191"
|
||||||
|
@input="form.job_title = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.job_title" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.job_title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date d'embauche -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Date d'embauche</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.hire_date"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.hire_date }"
|
||||||
|
type="date"
|
||||||
|
@input="form.hire_date = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.hire_date" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.hire_date }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Salaire</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.salary"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.salary }"
|
||||||
|
type="number"
|
||||||
|
placeholder="ex. 45000"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
@input="form.salary = $event.target.value"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.salary" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.salary }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statut actif -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
id="isActive"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.active"
|
||||||
|
@change="form.active = $event.target.checked"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="isActive">
|
||||||
|
Employé actif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons -->
|
||||||
|
<div class="button-row d-flex mt-4">
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
class="me-2 mb-0"
|
||||||
|
@click="resetForm"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="dark"
|
||||||
|
variant="gradient"
|
||||||
|
class="ms-auto mb-0"
|
||||||
|
:disabled="props.loading"
|
||||||
|
@click="submitForm"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="props.loading"
|
||||||
|
class="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
></span>
|
||||||
|
{{ props.loading ? "Création..." : "Créer l'employé" }}
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validationErrors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(["createEmployee"]);
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const errors = ref([]);
|
||||||
|
const fieldErrors = ref({});
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
job_title: "",
|
||||||
|
hire_date: "",
|
||||||
|
salary: null,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for validation errors from parent
|
||||||
|
watch(
|
||||||
|
() => props.validationErrors,
|
||||||
|
(newErrors) => {
|
||||||
|
fieldErrors.value = { ...newErrors };
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch for success from parent
|
||||||
|
watch(
|
||||||
|
() => props.success,
|
||||||
|
(newSuccess) => {
|
||||||
|
if (newSuccess) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
// Clear errors before submitting
|
||||||
|
fieldErrors.value = {};
|
||||||
|
errors.value = [];
|
||||||
|
|
||||||
|
// Clean up form data: convert empty strings to null
|
||||||
|
const cleanedForm = {};
|
||||||
|
const formData = form.value;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
if (value === "" || value === null || value === undefined) {
|
||||||
|
cleanedForm[key] = null;
|
||||||
|
} else {
|
||||||
|
cleanedForm[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure active is boolean
|
||||||
|
cleanedForm.active = Boolean(formData.active);
|
||||||
|
|
||||||
|
// Convert salary to number if provided
|
||||||
|
if (cleanedForm.salary !== null) {
|
||||||
|
cleanedForm.salary = parseFloat(cleanedForm.salary);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Form data being emitted:", cleanedForm);
|
||||||
|
|
||||||
|
// Emit the cleaned form data to parent
|
||||||
|
emit("createEmployee", cleanedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value = {
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
job_title: "",
|
||||||
|
hire_date: "",
|
||||||
|
salary: null,
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
clearErrors();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
errors.value = [];
|
||||||
|
fieldErrors.value = {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #f5365c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
thanasoft-front/src/components/templates/CRM/Employee
Normal file
23
thanasoft-front/src/components/templates/CRM/Employee
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<slot name="employee-new-action"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="dropdown d-inline">
|
||||||
|
<slot name="select-filter"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="employee-other-action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card mt-4">
|
||||||
|
<slot name="employee-table"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script></script>
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<slot name="employee-new-action"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="dropdown d-inline">
|
||||||
|
<slot name="select-filter"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="employee-other-action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card mt-4">
|
||||||
|
<slot name="employee-table"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script></script>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="multisteps-form mb-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-8 mx-auto my-5">
|
||||||
|
<slot name="multi-step" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--form panels-->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-8 m-auto">
|
||||||
|
<slot name="employee-form" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<p class="mb-0 font-weight-bold text-lg">Gestion des Employés</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body px-0 pt-0 pb-2">
|
||||||
|
<div class="table-responsive p-0">
|
||||||
|
<!-- Filter and Action Bar -->
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between align-items-center mb-4 px-4"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<slot name="select-filter">
|
||||||
|
<filter-table />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<slot name="employee-other-action">
|
||||||
|
<table-action />
|
||||||
|
</slot>
|
||||||
|
<slot name="employee-new-action">
|
||||||
|
<add-button text="Ajouter" @click="goToEmployee" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="employee-content">
|
||||||
|
<slot name="employee-table">
|
||||||
|
<!-- Default table slot - will be overridden by specific implementations -->
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goToEmployee = () => {
|
||||||
|
router.push({
|
||||||
|
name: "Creation employé",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container-fluid {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-lg {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-content-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-weight-bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #5e72e4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container-fluid {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex.gap-2 {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<slot name="product-category-new-action"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="dropdown d-inline">
|
||||||
|
<slot name="select-filter"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="product-category-other-action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card mt-4">
|
||||||
|
<slot name="product-category-table"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script></script>
|
||||||
@ -4,298 +4,54 @@
|
|||||||
class="w-auto h-auto collapse navbar-collapse max-height-vh-100 h-100"
|
class="w-auto h-auto collapse navbar-collapse max-height-vh-100 h-100"
|
||||||
>
|
>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<!-- Dashboard -->
|
<!-- Render navigation items from data array -->
|
||||||
<li class="nav-item">
|
<li
|
||||||
<sidenav-collapse
|
v-for="item in navigationItems"
|
||||||
collapse-ref="dashboardsExamples"
|
:key="item.id"
|
||||||
nav-text="Dashboards"
|
:class="{
|
||||||
:class="getRoute() === 'dashboards' ? 'active' : ''"
|
'nav-item': true,
|
||||||
>
|
'mt-3': item.type === 'section-header',
|
||||||
<template #icon>
|
}"
|
||||||
<Shop />
|
>
|
||||||
</template>
|
<!-- Section Header -->
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Default' }"
|
|
||||||
mini-icon="D"
|
|
||||||
text="Default"
|
|
||||||
/>
|
|
||||||
<sidenav-item :to="{ name: 'CRM' }" mini-icon="C" text="CRM" />
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Agenda -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-item :to="{ name: 'Agenda' }" mini-icon="A" text="Agenda">
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
</sidenav-item>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Courriel -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-item :to="{ name: 'Courriel' }" mini-icon="C" text="Courriel">
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
</sidenav-item>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion contacts' }"
|
|
||||||
mini-icon="C"
|
|
||||||
text="Contacts"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
</sidenav-item>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="mt-3 nav-item">
|
|
||||||
<h6
|
<h6
|
||||||
|
v-if="item.type === 'section-header'"
|
||||||
class="text-xs ps-4 text-uppercase font-weight-bolder opacity-6"
|
class="text-xs ps-4 text-uppercase font-weight-bolder opacity-6"
|
||||||
:class="isRTL ? 'me-4' : 'ms-2'"
|
:class="isRTL ? 'me-4' : 'ms-2'"
|
||||||
>
|
>
|
||||||
GESTION
|
{{ item.text }}
|
||||||
</h6>
|
</h6>
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Clients -->
|
<!-- Single Navigation Item -->
|
||||||
<li class="nav-item">
|
<sidenav-item
|
||||||
<sidenav-collapse
|
v-else-if="item.type === 'single'"
|
||||||
collapse-ref="clientsMenu"
|
:to="item.route"
|
||||||
nav-text="Clients"
|
:mini-icon="item.miniIcon"
|
||||||
:class="getRoute() === 'clients' ? 'active' : ''"
|
:text="item.text"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Office />
|
<component :is="getIconComponent(item.icon)" />
|
||||||
|
</template>
|
||||||
|
</sidenav-item>
|
||||||
|
|
||||||
|
<!-- Collapsible Navigation Item -->
|
||||||
|
<sidenav-collapse
|
||||||
|
v-else-if="item.type === 'collapse'"
|
||||||
|
:collapse-ref="item.collapseRef"
|
||||||
|
:nav-text="item.text"
|
||||||
|
:class="isItemActive(item) ? 'active' : ''"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<component :is="getIconComponent(item.icon)" />
|
||||||
</template>
|
</template>
|
||||||
<template #list>
|
<template #list>
|
||||||
<ul class="nav ms-4 ps-3">
|
<ul class="nav ms-4 ps-3">
|
||||||
<sidenav-item
|
<sidenav-item
|
||||||
:to="{ name: 'Gestion clients' }"
|
v-for="subItem in item.children"
|
||||||
mini-icon="C"
|
:key="subItem.id"
|
||||||
text="Clients"
|
:to="subItem.route"
|
||||||
/>
|
:mini-icon="subItem.miniIcon"
|
||||||
<sidenav-item
|
:text="subItem.text"
|
||||||
:to="{ name: 'Localisation clients' }"
|
|
||||||
mini-icon="L"
|
|
||||||
text="Lieux"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Statistiques clients' }"
|
|
||||||
mini-icon="S"
|
|
||||||
text="Statistiques"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Fournisseurs -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-collapse
|
|
||||||
collapse-ref="fournisseursMenu"
|
|
||||||
nav-text="Fournisseurs"
|
|
||||||
:class="getRoute() === 'fournisseurs' ? 'active' : ''"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion fournisseurs' }"
|
|
||||||
mini-icon="F"
|
|
||||||
text="Fournisseurs"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Commandes fournisseurs' }"
|
|
||||||
mini-icon="C"
|
|
||||||
text="Commandes"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Factures fournisseurs' }"
|
|
||||||
mini-icon="F"
|
|
||||||
text="Factures"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Statistiques fournisseurs' }"
|
|
||||||
mini-icon="S"
|
|
||||||
text="Statistiques"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Sous-Traitants -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-collapse
|
|
||||||
collapse-ref="sousTraitantsMenu"
|
|
||||||
nav-text="Sous-Traitants"
|
|
||||||
:class="getRoute() === 'sous-traitants' ? 'active' : ''"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion sous-traitants' }"
|
|
||||||
mini-icon="S"
|
|
||||||
text="Sous-Traitants"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Commandes sous-traitants' }"
|
|
||||||
mini-icon="C"
|
|
||||||
text="Commandes"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Factures sous-traitants' }"
|
|
||||||
mini-icon="F"
|
|
||||||
text="Factures"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Statistiques sous-traitants' }"
|
|
||||||
mini-icon="S"
|
|
||||||
text="Statistiques"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Ventes -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-collapse
|
|
||||||
collapse-ref="ventesMenu"
|
|
||||||
nav-text="Ventes"
|
|
||||||
:class="getRoute() === 'ventes' ? 'active' : ''"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Shop />
|
|
||||||
</template>
|
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Devis' }"
|
|
||||||
mini-icon="D"
|
|
||||||
text="Devis"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Factures ventes' }"
|
|
||||||
mini-icon="F"
|
|
||||||
text="Factures"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Statistiques ventes' }"
|
|
||||||
mini-icon="S"
|
|
||||||
text="Statistiques"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Stock -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-collapse
|
|
||||||
collapse-ref="stockMenu"
|
|
||||||
nav-text="Stock"
|
|
||||||
:class="getRoute() === 'stock' ? 'active' : ''"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion de produits' }"
|
|
||||||
mini-icon="R"
|
|
||||||
text="Produits"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Reception stock' }"
|
|
||||||
mini-icon="R"
|
|
||||||
text="Réception"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion stock' }"
|
|
||||||
mini-icon="S"
|
|
||||||
text="Stock"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Employés -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-collapse
|
|
||||||
collapse-ref="employesMenu"
|
|
||||||
nav-text="Employés"
|
|
||||||
:class="getRoute() === 'employes' ? 'active' : ''"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion employes' }"
|
|
||||||
mini-icon="E"
|
|
||||||
text="Employés"
|
|
||||||
/>
|
|
||||||
<sidenav-item :to="{ name: 'NDF' }" mini-icon="N" text="NDF" />
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Vehicules' }"
|
|
||||||
mini-icon="V"
|
|
||||||
text="Véhicules"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Absences' }"
|
|
||||||
mini-icon="A"
|
|
||||||
text="Absences"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</sidenav-collapse>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Paramétrage -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<sidenav-collapse
|
|
||||||
collapse-ref="parametrageMenu"
|
|
||||||
nav-text="Paramétrage"
|
|
||||||
:class="getRoute() === 'parametrage' ? 'active' : ''"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Office />
|
|
||||||
</template>
|
|
||||||
<template #list>
|
|
||||||
<ul class="nav ms-4 ps-3">
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion droits' }"
|
|
||||||
mini-icon="D"
|
|
||||||
text="Gestion des droits"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion emails' }"
|
|
||||||
mini-icon="E"
|
|
||||||
text="Gestion des emails"
|
|
||||||
/>
|
|
||||||
<sidenav-item
|
|
||||||
:to="{ name: 'Gestion modeles' }"
|
|
||||||
mini-icon="M"
|
|
||||||
text="Gestion des modèles"
|
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
@ -304,6 +60,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SidenavItem from "./SidenavItem.vue";
|
import SidenavItem from "./SidenavItem.vue";
|
||||||
import SidenavCollapse from "./SidenavCollapse.vue";
|
import SidenavCollapse from "./SidenavCollapse.vue";
|
||||||
@ -312,6 +69,7 @@ import Shop from "../../components/Icon/Shop.vue";
|
|||||||
import Office from "../../components/Icon/Office.vue";
|
import Office from "../../components/Icon/Office.vue";
|
||||||
|
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SidenavList",
|
name: "SidenavList",
|
||||||
components: {
|
components: {
|
||||||
@ -328,12 +86,303 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["isRTL"]),
|
...mapState(["isRTL"]),
|
||||||
|
|
||||||
|
// Navigation data structure
|
||||||
|
navigationItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "dashboard",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Dashboards",
|
||||||
|
icon: "Shop",
|
||||||
|
collapseRef: "dashboardsExamples",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "default",
|
||||||
|
route: { name: "Default" },
|
||||||
|
miniIcon: "D",
|
||||||
|
text: "Default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "crm-dashboard",
|
||||||
|
route: { name: "CRM" },
|
||||||
|
miniIcon: "C",
|
||||||
|
text: "CRM",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agenda",
|
||||||
|
type: "single",
|
||||||
|
text: "Agenda",
|
||||||
|
icon: "Office",
|
||||||
|
miniIcon: "A",
|
||||||
|
route: { name: "Agenda" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "courriel",
|
||||||
|
type: "single",
|
||||||
|
text: "Courriel",
|
||||||
|
icon: "Office",
|
||||||
|
miniIcon: "C",
|
||||||
|
route: { name: "Courriel" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contacts",
|
||||||
|
type: "single",
|
||||||
|
text: "Contacts",
|
||||||
|
icon: "Office",
|
||||||
|
miniIcon: "C",
|
||||||
|
route: { name: "Gestion contacts" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gestion-section",
|
||||||
|
type: "section-header",
|
||||||
|
text: "GESTION",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "clients",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Clients",
|
||||||
|
icon: "Office",
|
||||||
|
collapseRef: "clientsMenu",
|
||||||
|
routeKey: "clients",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "clients-list",
|
||||||
|
route: { name: "Gestion clients" },
|
||||||
|
miniIcon: "C",
|
||||||
|
text: "Clients",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "clients-locations",
|
||||||
|
route: { name: "Localisation clients" },
|
||||||
|
miniIcon: "L",
|
||||||
|
text: "Lieux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "clients-stats",
|
||||||
|
route: { name: "Statistiques clients" },
|
||||||
|
miniIcon: "S",
|
||||||
|
text: "Statistiques",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fournisseurs",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Fournisseurs",
|
||||||
|
icon: "Office",
|
||||||
|
collapseRef: "fournisseursMenu",
|
||||||
|
routeKey: "fournisseurs",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "fournisseurs-list",
|
||||||
|
route: { name: "Gestion fournisseurs" },
|
||||||
|
miniIcon: "F",
|
||||||
|
text: "Fournisseurs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fournisseurs-orders",
|
||||||
|
route: { name: "Commandes fournisseurs" },
|
||||||
|
miniIcon: "C",
|
||||||
|
text: "Commandes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fournisseurs-invoices",
|
||||||
|
route: { name: "Factures fournisseurs" },
|
||||||
|
miniIcon: "F",
|
||||||
|
text: "Factures",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fournisseurs-stats",
|
||||||
|
route: { name: "Statistiques fournisseurs" },
|
||||||
|
miniIcon: "S",
|
||||||
|
text: "Statistiques",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sous-traitants",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Sous-Traitants",
|
||||||
|
icon: "Office",
|
||||||
|
collapseRef: "sousTraitantsMenu",
|
||||||
|
routeKey: "sous-traitants",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "sous-traitants-list",
|
||||||
|
route: { name: "Gestion sous-traitants" },
|
||||||
|
miniIcon: "S",
|
||||||
|
text: "Sous-Traitants",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sous-traitants-orders",
|
||||||
|
route: { name: "Commandes sous-traitants" },
|
||||||
|
miniIcon: "C",
|
||||||
|
text: "Commandes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sous-traitants-invoices",
|
||||||
|
route: { name: "Factures sous-traitants" },
|
||||||
|
miniIcon: "F",
|
||||||
|
text: "Factures",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sous-traitants-stats",
|
||||||
|
route: { name: "Statistiques sous-traitants" },
|
||||||
|
miniIcon: "S",
|
||||||
|
text: "Statistiques",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ventes",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Ventes",
|
||||||
|
icon: "Shop",
|
||||||
|
collapseRef: "ventesMenu",
|
||||||
|
routeKey: "ventes",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "devis",
|
||||||
|
route: { name: "Devis" },
|
||||||
|
miniIcon: "D",
|
||||||
|
text: "Devis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "factures-ventes",
|
||||||
|
route: { name: "Factures ventes" },
|
||||||
|
miniIcon: "F",
|
||||||
|
text: "Factures",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ventes-stats",
|
||||||
|
route: { name: "Statistiques ventes" },
|
||||||
|
miniIcon: "S",
|
||||||
|
text: "Statistiques",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stock",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Stock",
|
||||||
|
icon: "Office",
|
||||||
|
collapseRef: "stockMenu",
|
||||||
|
routeKey: "stock",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "produits",
|
||||||
|
route: { name: "Gestion de produits" },
|
||||||
|
miniIcon: "R",
|
||||||
|
text: "Produits",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "reception",
|
||||||
|
route: { name: "Reception stock" },
|
||||||
|
miniIcon: "R",
|
||||||
|
text: "Réception",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stock-gestion",
|
||||||
|
route: { name: "Gestion stock" },
|
||||||
|
miniIcon: "S",
|
||||||
|
text: "Stock",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "employes",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Employés",
|
||||||
|
icon: "Office",
|
||||||
|
collapseRef: "employesMenu",
|
||||||
|
routeKey: "employes",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "employes-list",
|
||||||
|
route: { name: "Gestion employes" },
|
||||||
|
miniIcon: "E",
|
||||||
|
text: "Employés",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "employes-thanatopracteurs",
|
||||||
|
route: { name: "Employés Thanatopracteurs" },
|
||||||
|
miniIcon: "T",
|
||||||
|
text: "Employés & Thanatopracteurs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ndf",
|
||||||
|
route: { name: "NDF" },
|
||||||
|
miniIcon: "N",
|
||||||
|
text: "NDF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vehicules",
|
||||||
|
route: { name: "Véhicules" },
|
||||||
|
miniIcon: "V",
|
||||||
|
text: "Véhicules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "absences",
|
||||||
|
route: { name: "Absences" },
|
||||||
|
miniIcon: "A",
|
||||||
|
text: "Absences",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "parametrage",
|
||||||
|
type: "collapse",
|
||||||
|
text: "Paramétrage",
|
||||||
|
icon: "Office",
|
||||||
|
collapseRef: "parametrageMenu",
|
||||||
|
routeKey: "parametrage",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "droits",
|
||||||
|
route: { name: "Gestion droits" },
|
||||||
|
miniIcon: "D",
|
||||||
|
text: "Gestion des droits",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emails",
|
||||||
|
route: { name: "Gestion emails" },
|
||||||
|
miniIcon: "E",
|
||||||
|
text: "Gestion des emails",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "modeles",
|
||||||
|
route: { name: "Gestion modeles" },
|
||||||
|
miniIcon: "M",
|
||||||
|
text: "Gestion des modèles",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getRoute() {
|
getRoute() {
|
||||||
const routeArr = this.$route.path.split("/");
|
const routeArr = this.$route.path.split("/");
|
||||||
return routeArr[1];
|
return routeArr[1];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getIconComponent(iconName) {
|
||||||
|
const iconMap = {
|
||||||
|
Shop: "Shop",
|
||||||
|
Office: "Office",
|
||||||
|
};
|
||||||
|
return iconMap[iconName] || "Office";
|
||||||
|
},
|
||||||
|
|
||||||
|
isItemActive(item) {
|
||||||
|
if (item.type === "collapse" && item.routeKey) {
|
||||||
|
return this.getRoute() === item.routeKey;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -522,7 +522,12 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: "/employes",
|
path: "/employes",
|
||||||
name: "Gestion employes",
|
name: "Gestion employes",
|
||||||
component: () => import("@/views/pages/Employes/Employes.vue"),
|
component: () => import("@/views/pages/Employes/Employees.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/employes/new",
|
||||||
|
name: "Creation employé",
|
||||||
|
component: () => import("@/views/pages/CRM/AddEmployee.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/employes/ndf",
|
path: "/employes/ndf",
|
||||||
@ -531,7 +536,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/employes/vehicules",
|
path: "/employes/vehicules",
|
||||||
name: "Vehicules",
|
name: "Véhicules",
|
||||||
component: () => import("@/views/pages/Employes/Vehicules.vue"),
|
component: () => import("@/views/pages/Employes/Vehicules.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -539,6 +544,12 @@ const routes = [
|
|||||||
name: "Absences",
|
name: "Absences",
|
||||||
component: () => import("@/views/pages/Employes/Absences.vue"),
|
component: () => import("@/views/pages/Employes/Absences.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/employes/thanatopracteurs",
|
||||||
|
name: "Employés Thanatopracteurs",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/pages/Employes/EmployesThanatopracteurs.vue"),
|
||||||
|
},
|
||||||
// Paramétrage
|
// Paramétrage
|
||||||
{
|
{
|
||||||
path: "/parametrage/droits",
|
path: "/parametrage/droits",
|
||||||
|
|||||||
293
thanasoft-front/src/services/employee.ts
Normal file
293
thanasoft-front/src/services/employee.ts
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
export interface EmployeeAddress {
|
||||||
|
line1: string | null;
|
||||||
|
line2: string | null;
|
||||||
|
postal_code: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country_code: string | null;
|
||||||
|
full_address?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Employee {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
full_name: string;
|
||||||
|
email: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
hire_date: string;
|
||||||
|
job_title: string | null; // Changed from position to job_title
|
||||||
|
salary?: number | null; // Optional since API doesn't always return it
|
||||||
|
active: boolean; // Changed from is_active to active
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
// Relations
|
||||||
|
thanatopractitioner?: {
|
||||||
|
id: number;
|
||||||
|
license_number: string;
|
||||||
|
authorization_number: string;
|
||||||
|
authorization_valid_until: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeListResponse {
|
||||||
|
data: Employee[];
|
||||||
|
pagination: {
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
last_page: number;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For nested response structure
|
||||||
|
export interface NestedEmployeeListResponse {
|
||||||
|
data: {
|
||||||
|
data: Employee[];
|
||||||
|
pagination: {
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
last_page: number;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeResponse {
|
||||||
|
data: Employee;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEmployeePayload {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
hire_date: string;
|
||||||
|
job_title?: string | null; // Changed from position to job_title
|
||||||
|
salary?: number | null;
|
||||||
|
active?: boolean; // Changed from is_active to active
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEmployeePayload extends Partial<CreateEmployeePayload> {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmployeeService = {
|
||||||
|
/**
|
||||||
|
* Get all employees with pagination and filtering
|
||||||
|
*/
|
||||||
|
async getAllEmployees(params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
active?: boolean;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: string;
|
||||||
|
}): Promise<NestedEmployeeListResponse> {
|
||||||
|
const response = await request<NestedEmployeeListResponse>({
|
||||||
|
url: "/api/employees",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific employee by ID
|
||||||
|
*/
|
||||||
|
async getEmployee(id: number): Promise<EmployeeResponse> {
|
||||||
|
const response = await request<EmployeeResponse>({
|
||||||
|
url: `/api/employees/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new employee
|
||||||
|
*/
|
||||||
|
async createEmployee(
|
||||||
|
payload: CreateEmployeePayload
|
||||||
|
): Promise<EmployeeResponse> {
|
||||||
|
const formattedPayload = this.transformEmployeePayload(payload);
|
||||||
|
|
||||||
|
const response = await request<EmployeeResponse>({
|
||||||
|
url: "/api/employees",
|
||||||
|
method: "post",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing employee
|
||||||
|
*/
|
||||||
|
async updateEmployee(
|
||||||
|
payload: UpdateEmployeePayload
|
||||||
|
): Promise<EmployeeResponse> {
|
||||||
|
const { id, ...updateData } = payload;
|
||||||
|
const formattedPayload = this.transformEmployeePayload(updateData);
|
||||||
|
|
||||||
|
const response = await request<EmployeeResponse>({
|
||||||
|
url: `/api/employees/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an employee
|
||||||
|
*/
|
||||||
|
async deleteEmployee(
|
||||||
|
id: number
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
const response = await request<{ success: boolean; message: string }>({
|
||||||
|
url: `/api/employees/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active employees only
|
||||||
|
*/
|
||||||
|
async getActiveEmployees(params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
}): Promise<NestedEmployeeListResponse> {
|
||||||
|
const response = await request<NestedEmployeeListResponse>({
|
||||||
|
url: "/api/employees",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
active: true,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get employees with thanatopractitioner data
|
||||||
|
*/
|
||||||
|
async getEmployeesWithThanatopractitioner(): Promise<{
|
||||||
|
data: Employee[];
|
||||||
|
}> {
|
||||||
|
const response = await request<{
|
||||||
|
data: Employee[];
|
||||||
|
}>({
|
||||||
|
url: "/api/employees/with-thanatopractitioner",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get employee statistics
|
||||||
|
*/
|
||||||
|
async getStatistics(): Promise<{
|
||||||
|
data: {
|
||||||
|
total_employees: number;
|
||||||
|
active_employees: number;
|
||||||
|
inactive_employees: number;
|
||||||
|
total_salary: number;
|
||||||
|
average_salary: number;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const response = await request<{
|
||||||
|
data: {
|
||||||
|
total_employees: number;
|
||||||
|
active_employees: number;
|
||||||
|
inactive_employees: number;
|
||||||
|
total_salary: number;
|
||||||
|
average_salary: number;
|
||||||
|
};
|
||||||
|
}>({
|
||||||
|
url: "/api/employees/statistics",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform employee payload to match Laravel form request structure
|
||||||
|
*/
|
||||||
|
transformEmployeePayload(payload: Partial<CreateEmployeePayload>): any {
|
||||||
|
const transformed: any = { ...payload };
|
||||||
|
|
||||||
|
// Ensure boolean values are properly formatted
|
||||||
|
if (typeof transformed.active === "boolean") {
|
||||||
|
transformed.active = transformed.active ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format salary if present
|
||||||
|
if (transformed.salary !== undefined && transformed.salary !== null) {
|
||||||
|
transformed.salary = parseFloat(transformed.salary.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove undefined values to avoid sending them
|
||||||
|
Object.keys(transformed).forEach((key) => {
|
||||||
|
if (transformed[key] === undefined) {
|
||||||
|
delete transformed[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return transformed;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search employees by name, email, or other criteria
|
||||||
|
*/
|
||||||
|
async searchEmployees(query: string): Promise<Employee[]> {
|
||||||
|
const response = await request<{
|
||||||
|
data: {
|
||||||
|
data: Employee[];
|
||||||
|
};
|
||||||
|
}>({
|
||||||
|
url: "/api/employees",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
search: query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle employee active status
|
||||||
|
*/
|
||||||
|
async toggleEmployeeStatus(
|
||||||
|
id: number,
|
||||||
|
isActive: boolean
|
||||||
|
): Promise<EmployeeResponse> {
|
||||||
|
const response = await request<EmployeeResponse>({
|
||||||
|
url: `/api/employees/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: {
|
||||||
|
active: isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmployeeService;
|
||||||
@ -5,7 +5,12 @@ export interface Product {
|
|||||||
id: number;
|
id: number;
|
||||||
nom: string;
|
nom: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
categorie: string;
|
categorie_id: number;
|
||||||
|
category: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
fabricant: string | null;
|
fabricant: string | null;
|
||||||
stock_actuel: number;
|
stock_actuel: number;
|
||||||
stock_minimum: number;
|
stock_minimum: number;
|
||||||
@ -41,7 +46,7 @@ export interface Product {
|
|||||||
export interface ProductFormData {
|
export interface ProductFormData {
|
||||||
nom: string;
|
nom: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
categorie: string;
|
categorie_id: number;
|
||||||
fabricant?: string | null;
|
fabricant?: string | null;
|
||||||
stock_actuel: number;
|
stock_actuel: number;
|
||||||
stock_minimum: number;
|
stock_minimum: number;
|
||||||
@ -88,7 +93,7 @@ class ProductService {
|
|||||||
page?: number;
|
page?: number;
|
||||||
per_page?: number;
|
per_page?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
categorie?: string;
|
categorie_id?: number;
|
||||||
fournisseur_id?: number;
|
fournisseur_id?: number;
|
||||||
low_stock?: boolean;
|
low_stock?: boolean;
|
||||||
expiring_soon?: boolean;
|
expiring_soon?: boolean;
|
||||||
@ -185,7 +190,7 @@ class ProductService {
|
|||||||
|
|
||||||
// Get products by category
|
// Get products by category
|
||||||
async getProductsByCategory(
|
async getProductsByCategory(
|
||||||
category: string,
|
category: number,
|
||||||
params: { per_page?: number } = {}
|
params: { per_page?: number } = {}
|
||||||
): Promise<ProductListResponse> {
|
): Promise<ProductListResponse> {
|
||||||
const response = await request<ProductListResponse>({
|
const response = await request<ProductListResponse>({
|
||||||
|
|||||||
287
thanasoft-front/src/services/productCategorie.ts
Normal file
287
thanasoft-front/src/services/productCategorie.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
export interface ProductCategorie {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
reference: string;
|
||||||
|
categorie: string;
|
||||||
|
fabricant: string | null;
|
||||||
|
stock_actuel: number;
|
||||||
|
stock_minimum: number;
|
||||||
|
unite: string;
|
||||||
|
prix_unitaire: number;
|
||||||
|
date_expiration: string | null;
|
||||||
|
numero_lot: string | null;
|
||||||
|
conditionnement_nom: string | null;
|
||||||
|
conditionnement_quantite: number | null;
|
||||||
|
conditionnement_unite: string | null;
|
||||||
|
photo_url: string | null;
|
||||||
|
fiche_technique_url: string | null;
|
||||||
|
fournisseur_id: number | null;
|
||||||
|
is_low_stock: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
fournisseur?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
conditionnement?: {
|
||||||
|
nom: string | null;
|
||||||
|
quantite: number | null;
|
||||||
|
unite: string | null;
|
||||||
|
};
|
||||||
|
media?: {
|
||||||
|
photo_url: string | null;
|
||||||
|
fiche_technique_url: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategorieFormData {
|
||||||
|
nom: string;
|
||||||
|
reference: string;
|
||||||
|
categorie: string;
|
||||||
|
fabricant?: string | null;
|
||||||
|
stock_actuel: number;
|
||||||
|
stock_minimum: number;
|
||||||
|
unite: string;
|
||||||
|
prix_unitaire: number;
|
||||||
|
date_expiration?: string | null;
|
||||||
|
numero_lot?: string | null;
|
||||||
|
conditionnement_nom?: string | null;
|
||||||
|
conditionnement_quantite?: number | null;
|
||||||
|
conditionnement_unite?: string | null;
|
||||||
|
photo_url?: string | null;
|
||||||
|
fiche_technique_url?: string | null;
|
||||||
|
fournisseur_id?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategorieListResponse {
|
||||||
|
data: ProductCategorie[];
|
||||||
|
pagination: {
|
||||||
|
current_page: number;
|
||||||
|
from: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
to: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
summary: {
|
||||||
|
total_productCategories: number;
|
||||||
|
low_stock_productCategories: number;
|
||||||
|
total_value: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategorieStatistics {
|
||||||
|
total_productCategories: number;
|
||||||
|
low_stock_productCategories: number;
|
||||||
|
expiring_productCategories: number;
|
||||||
|
total_value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductCategorieService {
|
||||||
|
// Get all productCategories with pagination and filters
|
||||||
|
async getAllProductCategories(
|
||||||
|
params: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
categorie?: string;
|
||||||
|
fournisseur_id?: number;
|
||||||
|
low_stock?: boolean;
|
||||||
|
expiring_soon?: boolean;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: "asc" | "desc";
|
||||||
|
} = {}
|
||||||
|
): Promise<ProductCategorieListResponse> {
|
||||||
|
const response = await request<ProductCategorieListResponse>({
|
||||||
|
url: "/api/productCategories",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a single productCategorie by ID
|
||||||
|
async getProductCategorie(id: number): Promise<{ data: ProductCategorie }> {
|
||||||
|
const response = await request<{ data: ProductCategorie }>({
|
||||||
|
url: `/api/productCategories/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new productCategorie
|
||||||
|
async createProductCategorie(
|
||||||
|
productCategorieData: ProductCategorieFormData | FormData
|
||||||
|
): Promise<{ data: ProductCategorie }> {
|
||||||
|
let formattedPayload: any;
|
||||||
|
|
||||||
|
// Check if data is FormData (for file uploads)
|
||||||
|
if (productCategorieData instanceof FormData) {
|
||||||
|
formattedPayload = productCategorieData;
|
||||||
|
} else {
|
||||||
|
formattedPayload = this.transformProductCategoriePayload(
|
||||||
|
productCategorieData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request<{ data: ProductCategorie }>({
|
||||||
|
url: "/api/productCategories",
|
||||||
|
method: "post",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing productCategorie
|
||||||
|
async updateProductCategorie(
|
||||||
|
id: number,
|
||||||
|
productCategorieData: ProductCategorieFormData | FormData
|
||||||
|
): Promise<{ data: ProductCategorie }> {
|
||||||
|
let formattedPayload: any;
|
||||||
|
|
||||||
|
// Check if data is FormData (for file uploads)
|
||||||
|
if (productCategorieData instanceof FormData) {
|
||||||
|
formattedPayload = productCategorieData;
|
||||||
|
} else {
|
||||||
|
formattedPayload = this.transformProductCategoriePayload(
|
||||||
|
productCategorieData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request<{ data: ProductCategorie }>({
|
||||||
|
url: `/api/productCategories/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a productCategorie
|
||||||
|
async deleteProductCategorie(id: number): Promise<{ message: string }> {
|
||||||
|
const response = await request<{ message: string }>({
|
||||||
|
url: `/api/productCategories/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search productCategories by name
|
||||||
|
async searchProductCategories(
|
||||||
|
searchTerm: string,
|
||||||
|
exactMatch: boolean = false
|
||||||
|
): Promise<{ data: ProductCategorie[]; count: number; message: string }> {
|
||||||
|
const response = await request<{
|
||||||
|
data: ProductCategorie[];
|
||||||
|
count: number;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/productCategories/searchBy",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
name: searchTerm,
|
||||||
|
exact: exactMatch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get productCategories with low stock
|
||||||
|
async getLowStockProductCategories(
|
||||||
|
params: { per_page?: number } = {}
|
||||||
|
): Promise<ProductCategorieListResponse> {
|
||||||
|
const response = await request<ProductCategorieListResponse>({
|
||||||
|
url: "/api/productCategories/low-stock",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get productCategories by category
|
||||||
|
async getProductCategoriesByCategory(
|
||||||
|
category: string,
|
||||||
|
params: { per_page?: number } = {}
|
||||||
|
): Promise<ProductCategorieListResponse> {
|
||||||
|
const response = await request<ProductCategorieListResponse>({
|
||||||
|
url: "/api/productCategories/by-category",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
category,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get productCategorie statistics
|
||||||
|
async getProductCategorieStatistics(): Promise<{
|
||||||
|
data: ProductCategorieStatistics;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
const response = await request<{
|
||||||
|
data: ProductCategorieStatistics;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/productCategories/statistics",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stock quantity for a productCategorie
|
||||||
|
async updateStock(
|
||||||
|
productCategorieId: number,
|
||||||
|
newStock: number
|
||||||
|
): Promise<{ data: ProductCategorie; message: string }> {
|
||||||
|
const response = await request<{ data: ProductCategorie; message: string }>(
|
||||||
|
{
|
||||||
|
url: `/api/productCategories/${productCategorieId}/stock`,
|
||||||
|
method: "patch",
|
||||||
|
data: {
|
||||||
|
stock_actuel: newStock,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform productCategorie payload to match Laravel form request structure
|
||||||
|
*/
|
||||||
|
transformProductCategoriePayload(
|
||||||
|
payload: Partial<ProductCategorieFormData>
|
||||||
|
): any {
|
||||||
|
const transformed: any = { ...payload };
|
||||||
|
|
||||||
|
// Remove undefined values to avoid sending them
|
||||||
|
Object.keys(transformed).forEach((key) => {
|
||||||
|
if (transformed[key] === undefined) {
|
||||||
|
delete transformed[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods for productCategorie expiration checks
|
||||||
|
static isExpired(productCategorie: ProductCategorie): boolean {
|
||||||
|
if (!productCategorie.date_expiration) return false;
|
||||||
|
return new Date(productCategorie.date_expiration) < new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
static isExpiringSoon(
|
||||||
|
productCategorie: ProductCategorie,
|
||||||
|
days: number = 30
|
||||||
|
): boolean {
|
||||||
|
if (!productCategorie.date_expiration) return false;
|
||||||
|
const expirationDate = new Date(productCategorie.date_expiration);
|
||||||
|
const soonDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
||||||
|
return expirationDate <= soonDate && expirationDate >= new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCategorieService;
|
||||||
190
thanasoft-front/src/services/productCategory.ts
Normal file
190
thanasoft-front/src/services/productCategory.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
export interface ProductCategory {
|
||||||
|
id: number;
|
||||||
|
parent_id: number | null;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
active: boolean;
|
||||||
|
path: string;
|
||||||
|
has_children: boolean;
|
||||||
|
has_products: boolean;
|
||||||
|
children_count: number;
|
||||||
|
products_count: number;
|
||||||
|
parent?: ProductCategory;
|
||||||
|
children?: ProductCategory[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategoryFormData {
|
||||||
|
parent_id?: number | null;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategoryListResponse {
|
||||||
|
data: ProductCategory[];
|
||||||
|
pagination: {
|
||||||
|
current_page: number;
|
||||||
|
from: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
to: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategoryStatistics {
|
||||||
|
total_categories: number;
|
||||||
|
active_categories: number;
|
||||||
|
inactive_categories: number;
|
||||||
|
categories_with_children: number;
|
||||||
|
root_categories: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductCategoryService {
|
||||||
|
// Get all product categories with pagination and filters
|
||||||
|
async getAllProductCategories(
|
||||||
|
params: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
active?: boolean;
|
||||||
|
parent_id?: number;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: "asc" | "desc";
|
||||||
|
} = {}
|
||||||
|
): Promise<ProductCategoryListResponse> {
|
||||||
|
const response = await request<ProductCategoryListResponse>({
|
||||||
|
url: "/api/product-categories",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a single product category by ID
|
||||||
|
async getProductCategory(id: number): Promise<{ data: ProductCategory }> {
|
||||||
|
const response = await request<{ data: ProductCategory }>({
|
||||||
|
url: `/api/product-categories/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new product category
|
||||||
|
async createProductCategory(
|
||||||
|
categoryData: ProductCategoryFormData
|
||||||
|
): Promise<{ data: ProductCategory }> {
|
||||||
|
const response = await request<{ data: ProductCategory }>({
|
||||||
|
url: "/api/product-categories",
|
||||||
|
method: "post",
|
||||||
|
data: categoryData,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing product category
|
||||||
|
async updateProductCategory(
|
||||||
|
id: number,
|
||||||
|
categoryData: ProductCategoryFormData
|
||||||
|
): Promise<{ data: ProductCategory }> {
|
||||||
|
const response = await request<{ data: ProductCategory }>({
|
||||||
|
url: `/api/product-categories/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: categoryData,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a product category
|
||||||
|
async deleteProductCategory(id: number): Promise<{ message: string }> {
|
||||||
|
const response = await request<{ message: string }>({
|
||||||
|
url: `/api/product-categories/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search product categories
|
||||||
|
async searchProductCategories(
|
||||||
|
term: string,
|
||||||
|
params: { per_page?: number } = {}
|
||||||
|
): Promise<ProductCategoryListResponse> {
|
||||||
|
const response = await request<ProductCategoryListResponse>({
|
||||||
|
url: "/api/product-categories/search",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
term,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active product categories only
|
||||||
|
async getActiveProductCategories(): Promise<{ data: ProductCategory[] }> {
|
||||||
|
const response = await request<{ data: ProductCategory[] }>({
|
||||||
|
url: "/api/product-categories/active",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get root product categories (no parent)
|
||||||
|
async getRootProductCategories(): Promise<{ data: ProductCategory[] }> {
|
||||||
|
const response = await request<{ data: ProductCategory[] }>({
|
||||||
|
url: "/api/product-categories/roots",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get hierarchical structure
|
||||||
|
async getHierarchicalProductCategories(): Promise<{
|
||||||
|
data: ProductCategory[];
|
||||||
|
}> {
|
||||||
|
const response = await request<{ data: ProductCategory[] }>({
|
||||||
|
url: "/api/product-categories/hierarchical",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get product category statistics
|
||||||
|
async getProductCategoryStatistics(): Promise<{
|
||||||
|
data: ProductCategoryStatistics;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
const response = await request<{
|
||||||
|
data: ProductCategoryStatistics;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/product-categories/statistics",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle category active status
|
||||||
|
async toggleProductCategoryStatus(
|
||||||
|
id: number,
|
||||||
|
active: boolean
|
||||||
|
): Promise<{ data: ProductCategory; message: string }> {
|
||||||
|
const response = await request<{ data: ProductCategory; message: string }>({
|
||||||
|
url: `/api/product-categories/${id}/toggle-active`,
|
||||||
|
method: "patch",
|
||||||
|
data: {
|
||||||
|
active,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCategoryService;
|
||||||
373
thanasoft-front/src/stores/employeeStore.ts
Normal file
373
thanasoft-front/src/stores/employeeStore.ts
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import EmployeeService from "@/services/employee";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Employee,
|
||||||
|
CreateEmployeePayload,
|
||||||
|
UpdateEmployeePayload,
|
||||||
|
EmployeeListResponse,
|
||||||
|
NestedEmployeeListResponse,
|
||||||
|
} from "@/services/employee";
|
||||||
|
|
||||||
|
export const useEmployeeStore = defineStore("employee", () => {
|
||||||
|
// State
|
||||||
|
const employees = ref<Employee[]>([]);
|
||||||
|
const currentEmployee = ref<Employee | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const searchResults = ref<Employee[]>([]);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const pagination = ref({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const allEmployees = computed(() => employees.value);
|
||||||
|
const activeEmployees = computed(() =>
|
||||||
|
employees.value.filter((employee: Employee) => employee.active)
|
||||||
|
);
|
||||||
|
const inactiveEmployees = computed(() =>
|
||||||
|
employees.value.filter((employee: Employee) => !employee.active)
|
||||||
|
);
|
||||||
|
const isLoading = computed(() => loading.value);
|
||||||
|
const hasError = computed(() => error.value !== null);
|
||||||
|
const getError = computed(() => error.value);
|
||||||
|
const getEmployeeById = computed(() => (id: number) =>
|
||||||
|
employees.value.find((employee: Employee) => employee.id === id)
|
||||||
|
);
|
||||||
|
const getPagination = computed(() => pagination.value);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const setLoading = (isLoading: boolean) => {
|
||||||
|
loading.value = isLoading;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (err: string | null) => {
|
||||||
|
error.value = err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
error.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setEmployees = (newEmployees: Employee[]) => {
|
||||||
|
employees.value = newEmployees;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentEmployee = (employee: Employee | null) => {
|
||||||
|
currentEmployee.value = employee;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSearchEmployee = (searchEmployee: Employee[]) => {
|
||||||
|
searchResults.value = searchEmployee;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPagination = (meta: any) => {
|
||||||
|
if (meta) {
|
||||||
|
pagination.value = {
|
||||||
|
current_page: meta.current_page || 1,
|
||||||
|
last_page: meta.last_page || 1,
|
||||||
|
per_page: meta.per_page || 10,
|
||||||
|
total: meta.total || 0,
|
||||||
|
from: meta.from || 0,
|
||||||
|
to: meta.to || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all employees with optional pagination and filters
|
||||||
|
*/
|
||||||
|
const fetchEmployees = async (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
active?: boolean;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: string;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await EmployeeService.getAllEmployees(params);
|
||||||
|
setEmployees(response.data.data);
|
||||||
|
setPagination(response.data.pagination);
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch employees";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single employee by ID
|
||||||
|
*/
|
||||||
|
const fetchEmployee = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await EmployeeService.getEmployee(id);
|
||||||
|
setCurrentEmployee(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch employee";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new employee
|
||||||
|
*/
|
||||||
|
const createEmployee = async (payload: CreateEmployeePayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await EmployeeService.createEmployee(payload);
|
||||||
|
// Add the new employee to the list
|
||||||
|
employees.value.push(response.data);
|
||||||
|
setCurrentEmployee(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to create employee";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing employee
|
||||||
|
*/
|
||||||
|
const updateEmployee = async (payload: UpdateEmployeePayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(payload);
|
||||||
|
const response = await EmployeeService.updateEmployee(payload);
|
||||||
|
const updatedEmployee = response.data;
|
||||||
|
|
||||||
|
// Update in the employees list
|
||||||
|
const index = employees.value.findIndex(
|
||||||
|
(employee: Employee) => employee.id === updatedEmployee.id
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
employees.value[index] = updatedEmployee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current employee if it's the one being edited
|
||||||
|
if (
|
||||||
|
currentEmployee.value &&
|
||||||
|
currentEmployee.value.id === updatedEmployee.id
|
||||||
|
) {
|
||||||
|
setCurrentEmployee(updatedEmployee);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedEmployee;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to update employee";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an employee
|
||||||
|
*/
|
||||||
|
const deleteEmployee = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await EmployeeService.deleteEmployee(id);
|
||||||
|
|
||||||
|
// Remove from the employees list
|
||||||
|
employees.value = employees.value.filter(
|
||||||
|
(employee: Employee) => employee.id !== id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear current employee if it's the one being deleted
|
||||||
|
if (currentEmployee.value && currentEmployee.value.id === id) {
|
||||||
|
setCurrentEmployee(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to delete employee";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search employees
|
||||||
|
*/
|
||||||
|
const searchEmployees = async (query: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await EmployeeService.searchEmployees(query);
|
||||||
|
setSearchEmployee(results);
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
error.value = "Erreur lors de la recherche des employés";
|
||||||
|
console.error("Error searching employees:", err);
|
||||||
|
setSearchEmployee([]);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle employee active status
|
||||||
|
*/
|
||||||
|
const toggleEmployeeStatus = async (id: number, isActive: boolean) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await EmployeeService.toggleEmployeeStatus(id, isActive);
|
||||||
|
const updatedEmployee = response.data;
|
||||||
|
|
||||||
|
// Update in the employees list
|
||||||
|
const index = employees.value.findIndex(
|
||||||
|
(employee: Employee) => employee.id === id
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
employees.value[index] = updatedEmployee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current employee if it's the one being toggled
|
||||||
|
if (currentEmployee.value && currentEmployee.value.id === id) {
|
||||||
|
setCurrentEmployee(updatedEmployee);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedEmployee;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to toggle employee status";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get employee statistics
|
||||||
|
*/
|
||||||
|
const fetchStatistics = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await EmployeeService.getStatistics();
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch statistics";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear current employee
|
||||||
|
*/
|
||||||
|
const clearCurrentEmployee = () => {
|
||||||
|
setCurrentEmployee(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all state
|
||||||
|
*/
|
||||||
|
const clearStore = () => {
|
||||||
|
employees.value = [];
|
||||||
|
currentEmployee.value = null;
|
||||||
|
error.value = null;
|
||||||
|
pagination.value = {
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
employees,
|
||||||
|
currentEmployee,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
searchResults,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
allEmployees,
|
||||||
|
activeEmployees,
|
||||||
|
inactiveEmployees,
|
||||||
|
isLoading,
|
||||||
|
hasError,
|
||||||
|
getError,
|
||||||
|
getEmployeeById,
|
||||||
|
getPagination,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchEmployees,
|
||||||
|
fetchEmployee,
|
||||||
|
createEmployee,
|
||||||
|
updateEmployee,
|
||||||
|
deleteEmployee,
|
||||||
|
searchEmployees,
|
||||||
|
toggleEmployeeStatus,
|
||||||
|
fetchStatistics,
|
||||||
|
clearCurrentEmployee,
|
||||||
|
clearStore,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
});
|
||||||
510
thanasoft-front/src/stores/productCategoryStore.ts
Normal file
510
thanasoft-front/src/stores/productCategoryStore.ts
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import ProductCategoryService from "@/services/productCategory";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ProductCategory,
|
||||||
|
ProductCategoryFormData,
|
||||||
|
ProductCategoryListResponse,
|
||||||
|
} from "@/services/productCategory";
|
||||||
|
|
||||||
|
// Create an instance of productCategoryService
|
||||||
|
const productCategoryService = new ProductCategoryService();
|
||||||
|
|
||||||
|
export const useProductCategoryStore = defineStore("productCategory", () => {
|
||||||
|
// State
|
||||||
|
const productCategories = ref<ProductCategory[]>([]);
|
||||||
|
const currentProductCategory = ref<ProductCategory | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const pagination = ref({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Statistics state
|
||||||
|
const statistics = ref({
|
||||||
|
total_categories: 0,
|
||||||
|
active_categories: 0,
|
||||||
|
inactive_categories: 0,
|
||||||
|
categories_with_children: 0,
|
||||||
|
root_categories: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const allProductCategories = computed(() => productCategories.value);
|
||||||
|
|
||||||
|
const activeProductCategories = computed(() =>
|
||||||
|
productCategories.value.filter((category) => category.active)
|
||||||
|
);
|
||||||
|
|
||||||
|
const inactiveProductCategories = computed(() =>
|
||||||
|
productCategories.value.filter((category) => !category.active)
|
||||||
|
);
|
||||||
|
|
||||||
|
const rootProductCategories = computed(() =>
|
||||||
|
productCategories.value.filter((category) => !category.parent_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const parentProductCategories = computed(() =>
|
||||||
|
productCategories.value.filter((category) => category.has_children)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoading = computed(() => loading.value);
|
||||||
|
const hasError = computed(() => error.value !== null);
|
||||||
|
const getError = computed(() => error.value);
|
||||||
|
|
||||||
|
const getProductCategoryById = computed(() => (id: number) =>
|
||||||
|
productCategories.value.find((category) => category.id === id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const getPagination = computed(() => pagination.value);
|
||||||
|
const getStatistics = computed(() => statistics.value);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const setLoading = (isLoading: boolean) => {
|
||||||
|
loading.value = isLoading;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (err: string | null) => {
|
||||||
|
error.value = err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
error.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setProductCategories = (newProductCategories: ProductCategory[]) => {
|
||||||
|
productCategories.value = newProductCategories;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentProductCategory = (category: ProductCategory | null) => {
|
||||||
|
currentProductCategory.value = category;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPagination = (meta: any) => {
|
||||||
|
if (meta) {
|
||||||
|
pagination.value = {
|
||||||
|
current_page: meta.current_page || 1,
|
||||||
|
last_page: meta.last_page || 1,
|
||||||
|
per_page: meta.per_page || 10,
|
||||||
|
total: meta.total || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setStatistics = (stats: any) => {
|
||||||
|
if (stats) {
|
||||||
|
statistics.value = {
|
||||||
|
total_categories: stats.total_categories || 0,
|
||||||
|
active_categories: stats.active_categories || 0,
|
||||||
|
inactive_categories: stats.inactive_categories || 0,
|
||||||
|
categories_with_children: stats.categories_with_children || 0,
|
||||||
|
root_categories: stats.root_categories || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer toutes les catégories de produits avec pagination et filtres optionnels
|
||||||
|
*/
|
||||||
|
const fetchProductCategories = async (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
active?: boolean;
|
||||||
|
parent_id?: number;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: "asc" | "desc";
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.getAllProductCategories(
|
||||||
|
params
|
||||||
|
);
|
||||||
|
setProductCategories(response.data);
|
||||||
|
if (response.pagination) {
|
||||||
|
setPagination(response.pagination);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement des catégories de produits";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer une seule catégorie de produit par ID
|
||||||
|
*/
|
||||||
|
const fetchProductCategory = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.getProductCategory(id);
|
||||||
|
setCurrentProductCategory(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement de la catégorie de produit";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Créer une nouvelle catégorie de produit
|
||||||
|
*/
|
||||||
|
const createProductCategory = async (payload: ProductCategoryFormData) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.createProductCategory(
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
// Ajouter la nouvelle catégorie à la liste
|
||||||
|
productCategories.value.push(response.data);
|
||||||
|
setCurrentProductCategory(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec de la création de la catégorie de produit";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mettre à jour une catégorie de produit existante
|
||||||
|
*/
|
||||||
|
const updateProductCategory = async (
|
||||||
|
payload: ProductCategoryFormData & { id: number }
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.updateProductCategory(
|
||||||
|
payload.id,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
const updatedCategory = response.data;
|
||||||
|
|
||||||
|
// Mettre à jour dans la liste des catégories
|
||||||
|
const index = productCategories.value.findIndex(
|
||||||
|
(category) => category.id === updatedCategory.id
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
productCategories.value[index] = updatedCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour la catégorie actuelle si c'est celle en cours d'édition
|
||||||
|
if (
|
||||||
|
currentProductCategory.value &&
|
||||||
|
currentProductCategory.value.id === updatedCategory.id
|
||||||
|
) {
|
||||||
|
setCurrentProductCategory(updatedCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCategory;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec de la mise à jour de la catégorie de produit";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprimer une catégorie de produit
|
||||||
|
*/
|
||||||
|
const deleteProductCategory = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.deleteProductCategory(id);
|
||||||
|
|
||||||
|
// Retirer de la liste des catégories
|
||||||
|
productCategories.value = productCategories.value.filter(
|
||||||
|
(category) => category.id !== id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effacer la catégorie actuelle si c'est celle en cours de suppression
|
||||||
|
if (
|
||||||
|
currentProductCategory.value &&
|
||||||
|
currentProductCategory.value.id === id
|
||||||
|
) {
|
||||||
|
setCurrentProductCategory(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec de la suppression de la catégorie de produit";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechercher des catégories de produits
|
||||||
|
*/
|
||||||
|
const searchProductCategories = async (
|
||||||
|
term: string,
|
||||||
|
params?: {
|
||||||
|
per_page?: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.searchProductCategories(
|
||||||
|
term,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
setProductCategories(response.data);
|
||||||
|
if (response.pagination) {
|
||||||
|
setPagination(response.pagination);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec de la recherche de catégories de produits";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer uniquement les catégories actives
|
||||||
|
*/
|
||||||
|
const fetchActiveProductCategories = async (params?: {
|
||||||
|
per_page?: number;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.getActiveProductCategories();
|
||||||
|
setProductCategories(response.data);
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement des catégories actives";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les catégories racine
|
||||||
|
*/
|
||||||
|
const fetchRootProductCategories = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.getRootProductCategories();
|
||||||
|
setProductCategories(response.data);
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement des catégories racine";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer la structure hiérarchique
|
||||||
|
*/
|
||||||
|
const fetchHierarchicalProductCategories = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.getHierarchicalProductCategories();
|
||||||
|
setProductCategories(response.data);
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement de la structure hiérarchique";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les statistiques des catégories
|
||||||
|
*/
|
||||||
|
const fetchProductCategoryStatistics = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.getProductCategoryStatistics();
|
||||||
|
setStatistics(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement des statistiques";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basculer le statut actif/inactif d'une catégorie
|
||||||
|
*/
|
||||||
|
const toggleProductCategoryStatus = async (id: number, active: boolean) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await productCategoryService.toggleProductCategoryStatus(
|
||||||
|
id,
|
||||||
|
active
|
||||||
|
);
|
||||||
|
const updatedCategory = response.data;
|
||||||
|
|
||||||
|
// Mettre à jour dans la liste
|
||||||
|
const index = productCategories.value.findIndex(
|
||||||
|
(category) => category.id === updatedCategory.id
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
productCategories.value[index] = updatedCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCategory;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec de la mise à jour du statut";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialiser l'état
|
||||||
|
*/
|
||||||
|
const resetState = () => {
|
||||||
|
productCategories.value = [];
|
||||||
|
currentProductCategory.value = null;
|
||||||
|
loading.value = false;
|
||||||
|
error.value = null;
|
||||||
|
pagination.value = {
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
statistics.value = {
|
||||||
|
total_categories: 0,
|
||||||
|
active_categories: 0,
|
||||||
|
inactive_categories: 0,
|
||||||
|
categories_with_children: 0,
|
||||||
|
root_categories: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
productCategories,
|
||||||
|
currentProductCategory,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
pagination,
|
||||||
|
statistics,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
allProductCategories,
|
||||||
|
activeProductCategories,
|
||||||
|
inactiveProductCategories,
|
||||||
|
rootProductCategories,
|
||||||
|
parentProductCategories,
|
||||||
|
isLoading,
|
||||||
|
hasError,
|
||||||
|
getError,
|
||||||
|
getProductCategoryById,
|
||||||
|
getPagination,
|
||||||
|
getStatistics,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setLoading,
|
||||||
|
setError,
|
||||||
|
clearError,
|
||||||
|
setProductCategories,
|
||||||
|
setCurrentProductCategory,
|
||||||
|
setPagination,
|
||||||
|
setStatistics,
|
||||||
|
fetchProductCategories,
|
||||||
|
fetchProductCategory,
|
||||||
|
createProductCategory,
|
||||||
|
updateProductCategory,
|
||||||
|
deleteProductCategory,
|
||||||
|
searchProductCategories,
|
||||||
|
fetchActiveProductCategories,
|
||||||
|
fetchRootProductCategories,
|
||||||
|
fetchHierarchicalProductCategories,
|
||||||
|
fetchProductCategoryStatistics,
|
||||||
|
toggleProductCategoryStatus,
|
||||||
|
resetState,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useProductCategoryStore;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import ProductService from "@/services/product";
|
import ProductService from "@/services/product";
|
||||||
import type { Product } from "@/services/product";
|
import { Product } from "@/services/product";
|
||||||
|
|
||||||
interface Meta {
|
interface Meta {
|
||||||
current_page: number;
|
current_page: number;
|
||||||
@ -43,7 +43,7 @@ export const useProductStore = defineStore("product", {
|
|||||||
),
|
),
|
||||||
categories: (state) => {
|
categories: (state) => {
|
||||||
const categorySet = new Set(
|
const categorySet = new Set(
|
||||||
state.products.map((product) => product.categorie).filter(Boolean)
|
state.products.map((product) => product.categorie_id).filter(Boolean)
|
||||||
);
|
);
|
||||||
return Array.from(categorySet).sort();
|
return Array.from(categorySet).sort();
|
||||||
},
|
},
|
||||||
@ -212,12 +212,12 @@ export const useProductStore = defineStore("product", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchProductsByCategory(category: string) {
|
async fetchProductsByCategory(categoryId: number) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const products = await productService.getProductsByCategory(category);
|
const products = await productService.getProductsByCategory(categoryId);
|
||||||
return products.data;
|
return products.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.error =
|
this.error =
|
||||||
@ -269,9 +269,11 @@ export const useProductStore = defineStore("product", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Local filtering functions
|
// Local filtering functions
|
||||||
filterByCategory(category: string) {
|
filterByCategory(categoryId: number) {
|
||||||
if (!category) return this.products;
|
if (!categoryId) return this.products;
|
||||||
return this.products.filter((product) => product.categorie === category);
|
return this.products.filter(
|
||||||
|
(product) => product.categorie_id === categoryId
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
filterByLowStock() {
|
filterByLowStock() {
|
||||||
|
|||||||
59
thanasoft-front/src/views/pages/CRM/AddEmployee.vue
Normal file
59
thanasoft-front/src/views/pages/CRM/AddEmployee.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<add-employee-presentation
|
||||||
|
:loading="employeeStore.isLoading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="showSuccess"
|
||||||
|
@create-employee="handleCreateEmployee"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import AddEmployeePresentation from "@/components/Organism/CRM/AddEmployeePresentation.vue";
|
||||||
|
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const employeeStore = useEmployeeStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const validationErrors = ref({});
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
|
const handleCreateEmployee = async (form) => {
|
||||||
|
try {
|
||||||
|
// Clear previous errors
|
||||||
|
validationErrors.value = {};
|
||||||
|
showSuccess.value = false;
|
||||||
|
|
||||||
|
// Call the store to create employee
|
||||||
|
const employee = await employeeStore.createEmployee(form);
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
notificationStore.created("Employé");
|
||||||
|
showSuccess.value = true;
|
||||||
|
|
||||||
|
// Redirect after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: "Gestion employés" });
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating employee:", error);
|
||||||
|
|
||||||
|
// Handle validation errors from Laravel
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
validationErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
|
} else if (error.response && error.response.data) {
|
||||||
|
// Handle other API errors
|
||||||
|
const errorMessage =
|
||||||
|
error.response.data.message || "Une erreur est survenue";
|
||||||
|
notificationStore.error("Erreur", errorMessage);
|
||||||
|
} else {
|
||||||
|
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
53
thanasoft-front/src/views/pages/Employes/Employees.vue
Normal file
53
thanasoft-front/src/views/pages/Employes/Employees.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<employee-presentation
|
||||||
|
:employee-data="employeeStore.employees"
|
||||||
|
:loading-data="employeeStore.loading"
|
||||||
|
:pagination="employeeStore.pagination"
|
||||||
|
@push-details="goDetails"
|
||||||
|
@delete-employee="deleteEmployee"
|
||||||
|
@change-page="changePage"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
|
||||||
|
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const employeeStore = useEmployeeStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await employeeStore.fetchEmployees();
|
||||||
|
});
|
||||||
|
|
||||||
|
const goDetails = (id) => {
|
||||||
|
router.push({
|
||||||
|
name: "Employee details",
|
||||||
|
params: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEmployee = async (employeeId) => {
|
||||||
|
try {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cet employé ?")) {
|
||||||
|
await employeeStore.deleteEmployee(employeeId);
|
||||||
|
alert("Employé supprimé avec succès");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting employee:", error);
|
||||||
|
alert("Erreur lors de la suppression de l'employé");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePage = async (page) => {
|
||||||
|
try {
|
||||||
|
await employeeStore.fetchEmployees({ page });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing page:", error);
|
||||||
|
alert("Erreur lors du changement de page");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h1>Gestion employes</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "GestionEmployes",
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<div class="d-lg-flex">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-0">Employés & Thanatopracteurs</h1>
|
||||||
|
<p class="text-sm mb-0">
|
||||||
|
Gestion des employés et thanatopracticiens de l'entreprise
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ms-auto my-auto mt-lg-0 mt-4">
|
||||||
|
<div class="ms-auto my-auto">
|
||||||
|
<button class="btn btn-primary btn-sm mb-0">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
Ajouter un employé
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body px-0 pb-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<h3>Module en développement</h3>
|
||||||
|
<p class="text-sm text-muted">
|
||||||
|
Cette section sera bientôt disponible avec toutes les
|
||||||
|
fonctionnalités de gestion des employés et thanatopracticiens.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "EmployesThanatopracteurs",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -4,6 +4,7 @@
|
|||||||
:loading="productStore.isLoading"
|
:loading="productStore.isLoading"
|
||||||
:validation-errors="validationErrors"
|
:validation-errors="validationErrors"
|
||||||
:success="showSuccess"
|
:success="showSuccess"
|
||||||
|
:categories="productCategoryStore.productCategories"
|
||||||
@create-product="handleCreateProduct"
|
@create-product="handleCreateProduct"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -13,6 +14,7 @@ import AddProductPresentation from "@/components/Organism/Stock/AddProductPresen
|
|||||||
import { useFournisseurStore } from "@/stores/fournisseurStore";
|
import { useFournisseurStore } from "@/stores/fournisseurStore";
|
||||||
import { useProductStore } from "@/stores/productStore";
|
import { useProductStore } from "@/stores/productStore";
|
||||||
import { useNotificationStore } from "@/stores/notification";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
@ -20,12 +22,13 @@ const router = useRouter();
|
|||||||
const fournisseurStore = useFournisseurStore();
|
const fournisseurStore = useFournisseurStore();
|
||||||
const productStore = useProductStore();
|
const productStore = useProductStore();
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
const validationErrors = ref({});
|
const validationErrors = ref({});
|
||||||
const showSuccess = ref(false);
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Load fournisseurs for the supplier dropdown
|
|
||||||
await fournisseurStore.fetchFournisseurs();
|
await fournisseurStore.fetchFournisseurs();
|
||||||
|
await productCategoryStore.fetchProductCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateProduct = async (form) => {
|
const handleCreateProduct = async (form) => {
|
||||||
|
|||||||
72
thanasoft-front/src/views/pages/Stock/AddProductCategory.vue
Normal file
72
thanasoft-front/src/views/pages/Stock/AddProductCategory.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<add-product-category-presentation
|
||||||
|
:parent-categories="parentCategories"
|
||||||
|
:loading="productCategoryStore.isLoading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="showSuccess"
|
||||||
|
@create-category="handleCreateCategory"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AddProductCategoryPresentation from "@/components/Organism/Stock/AddProductCategoryPresentation.vue";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { onMounted, ref, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const validationErrors = ref({});
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
|
// Get parent categories for dropdown selection
|
||||||
|
const parentCategories = computed(
|
||||||
|
() => productCategoryStore.rootProductCategories
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Load parent categories for the dropdown
|
||||||
|
await productCategoryStore.fetchRootProductCategories();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateCategory = async (form) => {
|
||||||
|
try {
|
||||||
|
console.log(form);
|
||||||
|
// Clear previous errors
|
||||||
|
validationErrors.value = {};
|
||||||
|
showSuccess.value = false;
|
||||||
|
|
||||||
|
// Call the store to create category
|
||||||
|
const category = await productCategoryStore.createProductCategory(form);
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
notificationStore.created("Catégorie de produit");
|
||||||
|
showSuccess.value = true;
|
||||||
|
|
||||||
|
// Redirect after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: "Gestion catégories de produits" });
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating product category:", error);
|
||||||
|
|
||||||
|
// Handle validation errors from Laravel
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
validationErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
|
} else if (error.response && error.response.data) {
|
||||||
|
// Handle other API errors
|
||||||
|
const errorMessage =
|
||||||
|
error.response.data.message || "Une erreur est survenue";
|
||||||
|
notificationStore.error("Erreur", errorMessage);
|
||||||
|
} else {
|
||||||
|
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<edit-product-category-presentation
|
||||||
|
:category-data="categoryData"
|
||||||
|
:parent-categories="parentCategories"
|
||||||
|
:loading="productCategoryStore.isLoading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="showSuccess"
|
||||||
|
@update-category="handleUpdateCategory"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import EditProductCategoryPresentation from "@/components/Organism/Stock/EditProductCategoryPresentation.vue";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { onMounted, ref, computed } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const validationErrors = ref({});
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
const categoryData = ref(null);
|
||||||
|
|
||||||
|
// Get parent categories for dropdown selection (excluding current category to avoid circular reference)
|
||||||
|
const parentCategories = computed(() => {
|
||||||
|
if (!categoryData.value) return [];
|
||||||
|
|
||||||
|
return productCategoryStore.rootProductCategories.filter(
|
||||||
|
(category) => category.id !== categoryData.value.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const categoryId = route.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load the category data to edit
|
||||||
|
categoryData.value = await productCategoryStore.fetchProductCategory(
|
||||||
|
categoryId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load parent categories for the dropdown
|
||||||
|
await productCategoryStore.fetchRootProductCategories();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading product category:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de charger la catégorie");
|
||||||
|
router.push({ name: "Gestion catégories de produits" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateCategory = async (form) => {
|
||||||
|
try {
|
||||||
|
console.log(form);
|
||||||
|
// Clear previous errors
|
||||||
|
validationErrors.value = {};
|
||||||
|
showSuccess.value = false;
|
||||||
|
|
||||||
|
// Call the store to update category
|
||||||
|
const category = await productCategoryStore.updateProductCategory({
|
||||||
|
...form,
|
||||||
|
id: categoryData.value.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
notificationStore.updated("Catégorie de produit");
|
||||||
|
showSuccess.value = true;
|
||||||
|
|
||||||
|
// Redirect after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({
|
||||||
|
name: "Détails catégorie produit",
|
||||||
|
params: { id: categoryData.value.id },
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating product category:", error);
|
||||||
|
|
||||||
|
// Handle validation errors from Laravel
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
validationErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
|
} else if (error.response && error.response.data) {
|
||||||
|
// Handle other API errors
|
||||||
|
const errorMessage =
|
||||||
|
error.response.data.message || "Une erreur est survenue";
|
||||||
|
notificationStore.error("Erreur", errorMessage);
|
||||||
|
} else {
|
||||||
|
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
42
thanasoft-front/src/views/pages/Stock/ProductCategories.vue
Normal file
42
thanasoft-front/src/views/pages/Stock/ProductCategories.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<product-category-presentation
|
||||||
|
:category-data="productCategoryStore.productCategories"
|
||||||
|
:loading-data="productCategoryStore.loading"
|
||||||
|
@push-details="goDetails"
|
||||||
|
@delete-category="deleteCategory"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import ProductCategoryPresentation from "@/components/Organism/Stock/ProductCategoryPresentation.vue";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await productCategoryStore.fetchProductCategories();
|
||||||
|
});
|
||||||
|
|
||||||
|
const goDetails = (id) => {
|
||||||
|
router.push({
|
||||||
|
name: "Détails catégorie produit",
|
||||||
|
params: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCategory = async (id) => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cette catégorie ?")) {
|
||||||
|
try {
|
||||||
|
await productCategoryStore.deleteProductCategory(id);
|
||||||
|
alert("Catégorie supprimée avec succès");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression:", error);
|
||||||
|
alert("Erreur lors de la suppression de la catégorie");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<product-category-details-presentation
|
||||||
|
v-if="categoryData"
|
||||||
|
:category-data="categoryData"
|
||||||
|
:loading="productCategoryStore.isLoading"
|
||||||
|
:statistics="productCategoryStore.getStatistics"
|
||||||
|
@edit="goToEdit"
|
||||||
|
@delete="deleteCategory"
|
||||||
|
@back="goBack"
|
||||||
|
/>
|
||||||
|
<div v-else-if="productCategoryStore.isLoading" class="text-center py-4">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-4">
|
||||||
|
<p>Catégorie non trouvée</p>
|
||||||
|
<button @click="goBack" class="btn btn-primary">Retour</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ProductCategoryDetailsPresentation from "@/components/Organism/Stock/ProductCategoryDetailsPresentation.vue";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const categoryData = ref(null);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const categoryId = route.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load the category data
|
||||||
|
categoryData.value = await productCategoryStore.fetchProductCategory(
|
||||||
|
categoryId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load statistics
|
||||||
|
await productCategoryStore.fetchProductCategoryStatistics();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading product category:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de charger la catégorie");
|
||||||
|
|
||||||
|
// Redirect back to categories list if category not found
|
||||||
|
if (error.response && error.response.status === 404) {
|
||||||
|
router.push({ name: "Gestion catégories de produits" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToEdit = (id) => {
|
||||||
|
router.push({
|
||||||
|
name: "Modification catégorie produit",
|
||||||
|
params: { id: id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCategory = async (id) => {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Êtes-vous sûr de vouloir supprimer cette catégorie ? Cette action est irréversible."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await productCategoryStore.deleteProductCategory(id);
|
||||||
|
notificationStore.deleted("Catégorie de produit");
|
||||||
|
router.push({ name: "Gestion catégories de produits" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting product category:", error);
|
||||||
|
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
notificationStore.error(
|
||||||
|
"Suppression impossible",
|
||||||
|
"Cette catégorie contient des produits ou des sous-catégories"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de supprimer la catégorie"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push({ name: "Gestion catégories de produits" });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user