From 0ea8f1866b97762c376170850caca28f97448c7c Mon Sep 17 00:00:00 2001
From: Nyavokevin <42602932+nyavokevin@users.noreply.github.com>
Date: Wed, 5 Nov 2025 17:08:08 +0300
Subject: [PATCH] add employee
---
.../Organism/CRM/AddEmployeePresentation.vue | 310 +++++++++
.../Employee/EmployeePresentation.vue | 228 +++++++
.../Stock/ProductCategoryPresentation.vue | 175 +++++
.../molecules/Employees/EmployeeTable.vue | 507 +++++++++++++++
.../molecules/Stock/ProductCategoryModal.vue | 473 ++++++++++++++
.../Tables/Stock/ProductCategoryTable | 516 +++++++++++++++
.../Tables/Stock/ProductCategoryTable.vue | 532 +++++++++++++++
.../molecules/form/NewEmployeeForm.vue | 311 +++++++++
.../src/components/templates/CRM/Employee | 23 +
.../templates/CRM/EmployeeTemplate.vue | 23 +
.../templates/CRM/NewEmployeeTemplate.vue | 21 +
.../templates/Employee/EmployeeTemplate.vue | 141 ++++
.../Stock/ProductCategoryTemplate.vue | 23 +
.../src/examples/Sidenav/SidenavList.vue | 609 ++++++++++--------
thanasoft-front/src/router/index.js | 15 +-
thanasoft-front/src/services/employee.ts | 293 +++++++++
thanasoft-front/src/services/product.ts | 13 +-
.../src/services/productCategorie.ts | 287 +++++++++
.../src/services/productCategory.ts | 190 ++++++
thanasoft-front/src/stores/employeeStore.ts | 373 +++++++++++
.../src/stores/productCategoryStore.ts | 510 +++++++++++++++
thanasoft-front/src/stores/productStore.ts | 16 +-
.../src/views/pages/CRM/AddEmployee.vue | 59 ++
.../src/views/pages/Employes/Employees.vue | 53 ++
.../src/views/pages/Employes/Employes.vue | 11 -
.../Employes/EmployesThanatopracteurs.vue | 45 ++
.../src/views/pages/Stock/AddProduct.vue | 5 +-
.../views/pages/Stock/AddProductCategory.vue | 72 +++
.../views/pages/Stock/EditProductCategory.vue | 98 +++
.../views/pages/Stock/ProductCategories.vue | 42 ++
.../pages/Stock/ProductCategoryDetails.vue | 95 +++
31 files changed, 5764 insertions(+), 305 deletions(-)
create mode 100644 thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue
create mode 100644 thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue
create mode 100644 thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue
create mode 100644 thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue
create mode 100644 thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue
create mode 100644 thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable
create mode 100644 thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue
create mode 100644 thanasoft-front/src/components/molecules/form/NewEmployeeForm.vue
create mode 100644 thanasoft-front/src/components/templates/CRM/Employee
create mode 100644 thanasoft-front/src/components/templates/CRM/EmployeeTemplate.vue
create mode 100644 thanasoft-front/src/components/templates/CRM/NewEmployeeTemplate.vue
create mode 100644 thanasoft-front/src/components/templates/Employee/EmployeeTemplate.vue
create mode 100644 thanasoft-front/src/components/templates/Stock/ProductCategoryTemplate.vue
create mode 100644 thanasoft-front/src/services/employee.ts
create mode 100644 thanasoft-front/src/services/productCategorie.ts
create mode 100644 thanasoft-front/src/services/productCategory.ts
create mode 100644 thanasoft-front/src/stores/employeeStore.ts
create mode 100644 thanasoft-front/src/stores/productCategoryStore.ts
create mode 100644 thanasoft-front/src/views/pages/CRM/AddEmployee.vue
create mode 100644 thanasoft-front/src/views/pages/Employes/Employees.vue
delete mode 100644 thanasoft-front/src/views/pages/Employes/Employes.vue
create mode 100644 thanasoft-front/src/views/pages/Employes/EmployesThanatopracteurs.vue
create mode 100644 thanasoft-front/src/views/pages/Stock/AddProductCategory.vue
create mode 100644 thanasoft-front/src/views/pages/Stock/EditProductCategory.vue
create mode 100644 thanasoft-front/src/views/pages/Stock/ProductCategories.vue
create mode 100644 thanasoft-front/src/views/pages/Stock/ProductCategoryDetails.vue
diff --git a/thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue b/thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue
new file mode 100644
index 0000000..3d159a6
--- /dev/null
+++ b/thanasoft-front/src/components/Organism/CRM/AddEmployeePresentation.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Succès! L'employé a été créé avec succès.
+ Redirection en cours...
+
+
+
+
+
+ Création en cours...
+
+
Création de l'employé...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue b/thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue
new file mode 100644
index 0000000..b1c50eb
--- /dev/null
+++ b/thanasoft-front/src/components/Organism/Employee/EmployeePresentation.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Affichage de {{ pagination.from }} à {{ pagination.to }} sur
+ {{ pagination.total }} employés
+
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue b/thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue
new file mode 100644
index 0000000..e211444
--- /dev/null
+++ b/thanasoft-front/src/components/Organism/Stock/ProductCategoryPresentation.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue b/thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue
new file mode 100644
index 0000000..5a56c33
--- /dev/null
+++ b/thanasoft-front/src/components/molecules/Employees/EmployeeTable.vue
@@ -0,0 +1,507 @@
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nom & Prénom |
+ Email |
+ Téléphone |
+ Poste |
+ Date d'embauche |
+ Status |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nom & Prénom |
+ Email |
+ Téléphone |
+ Poste |
+ Date d'embauche |
+ Status |
+ Action |
+
+
+
+
+
+
+
+
+
+ {{ employee.id }}
+
+
+ |
+
+
+
+
+
+
+ {{ employee.last_name }} {{ employee.first_name }}
+
+ Thanatopractitioner
+
+
+
+ |
+
+
+
+ {{ employee.email || "N/A" }}
+ |
+
+
+
+ {{ employee.phone || "N/A" }}
+ |
+
+
+
+
+
+
+
+ {{ employee.job_title || "N/A" }}
+
+ |
+
+
+
+ {{ formatDate(employee.hire_date) }}
+ |
+
+
+
+
+
+
+
+ {{ employee.active ? "Actif" : "Inactif" }}
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
Aucun employé trouvé
+
+ Aucun employé à afficher pour le moment.
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue b/thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue
new file mode 100644
index 0000000..ef8f138
--- /dev/null
+++ b/thanasoft-front/src/components/molecules/Stock/ProductCategoryModal.vue
@@ -0,0 +1,473 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable b/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable
new file mode 100644
index 0000000..9942bf5
--- /dev/null
+++ b/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable
@@ -0,0 +1,516 @@
+
+
+
+
+
+
+
+
+
+
+
+ | Code |
+ Nom |
+ Catégorie parent |
+ Description |
+ Statut |
+ Produits |
+ Actions |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ | Code |
+ Nom |
+ Catégorie parent |
+ Description |
+ Statut |
+ Produits |
+ Actions |
+
+
+
+
+
+ |
+
+ {{ category.code }}
+
+ |
+
+
+
+
+
+
+ {{ category.name }}
+
+ ID: {{ category.id }}
+
+
+ |
+
+
+
+
+
+
+
+ {{ category.parent.name }}
+
+
+
+
+
+ Catégorie racine
+
+ |
+
+
+
+
+
+ {{ category.description }}
+
+ Aucune description
+
+ |
+
+
+
+
+
+
+
+ {{ category.active ? "Active" : "Inactive" }}
+
+
+
+
+
+ {{ category.children_count || 0 }} enfants
+
+
+ |
+
+
+
+
+
+ {{ category.products_count || 0 }}
+
+ produits
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
Aucune catégorie de produit trouvée
+
+ Aucune catégorie de produit à afficher pour le moment.
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue b/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue
new file mode 100644
index 0000000..688ecc3
--- /dev/null
+++ b/thanasoft-front/src/components/molecules/Tables/Stock/ProductCategoryTable.vue
@@ -0,0 +1,532 @@
+
+
+
+
+
+
+
+
+
+
+
+ | Code |
+ Nom |
+ Catégorie parent |
+ Description |
+ Statut |
+ Produits |
+ Action |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ | Code |
+ Nom |
+ Catégorie parent |
+ Description |
+ Statut |
+ Produits |
+ Action |
+
+
+
+
+
+ |
+
+
+
+
+ {{ category.code }}
+
+ |
+
+
+
+
+
+
+
+ {{ category.name }}
+
+ ID: {{ category.id }}
+
+
+ |
+
+
+
+
+
+
+
+ {{ category.parent.name }}
+
+
+
+ Catégorie racine
+
+ |
+
+
+
+
+
+ {{ category.description }}
+
+ Aucune description
+
+ |
+
+
+
+
+
+
+
+ Parent
+
+
+
+
+
+ Utilisée
+
+
+
+
+
+ Libre
+
+
+ |
+
+
+
+
+ {{ category.products_count || 0 }}
+ produits
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
Aucune catégorie de produit trouvée
+
+ Aucune catégorie de produit à afficher pour le moment.
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/molecules/form/NewEmployeeForm.vue b/thanasoft-front/src/components/molecules/form/NewEmployeeForm.vue
new file mode 100644
index 0000000..ceeb331
--- /dev/null
+++ b/thanasoft-front/src/components/molecules/form/NewEmployeeForm.vue
@@ -0,0 +1,311 @@
+
+
+
Nouvel Employé
+
Informations de l'employé
+
+
+
+
+
+
+
+
+ {{ fieldErrors.first_name }}
+
+
+
+
+
+
+ {{ fieldErrors.last_name }}
+
+
+
+
+
+
+
+
+
+
+ {{ fieldErrors.email }}
+
+
+
+
+
+
+ {{ fieldErrors.phone }}
+
+
+
+
+
+
+
+
+
+
+ {{ fieldErrors.job_title }}
+
+
+
+
+
+
+
+
+
+
+ {{ fieldErrors.hire_date }}
+
+
+
+
+
+
+ {{ fieldErrors.salary }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Réinitialiser
+
+
+
+ {{ props.loading ? "Création..." : "Créer l'employé" }}
+
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/templates/CRM/Employee b/thanasoft-front/src/components/templates/CRM/Employee
new file mode 100644
index 0000000..fe2a9d3
--- /dev/null
+++ b/thanasoft-front/src/components/templates/CRM/Employee
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/thanasoft-front/src/components/templates/CRM/EmployeeTemplate.vue b/thanasoft-front/src/components/templates/CRM/EmployeeTemplate.vue
new file mode 100644
index 0000000..fe2a9d3
--- /dev/null
+++ b/thanasoft-front/src/components/templates/CRM/EmployeeTemplate.vue
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/thanasoft-front/src/components/templates/CRM/NewEmployeeTemplate.vue b/thanasoft-front/src/components/templates/CRM/NewEmployeeTemplate.vue
new file mode 100644
index 0000000..b5348a6
--- /dev/null
+++ b/thanasoft-front/src/components/templates/CRM/NewEmployeeTemplate.vue
@@ -0,0 +1,21 @@
+
+
+
diff --git a/thanasoft-front/src/components/templates/Employee/EmployeeTemplate.vue b/thanasoft-front/src/components/templates/Employee/EmployeeTemplate.vue
new file mode 100644
index 0000000..27a9551
--- /dev/null
+++ b/thanasoft-front/src/components/templates/Employee/EmployeeTemplate.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/components/templates/Stock/ProductCategoryTemplate.vue b/thanasoft-front/src/components/templates/Stock/ProductCategoryTemplate.vue
new file mode 100644
index 0000000..9d2395e
--- /dev/null
+++ b/thanasoft-front/src/components/templates/Stock/ProductCategoryTemplate.vue
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/thanasoft-front/src/examples/Sidenav/SidenavList.vue b/thanasoft-front/src/examples/Sidenav/SidenavList.vue
index d5d043c..7539f90 100644
--- a/thanasoft-front/src/examples/Sidenav/SidenavList.vue
+++ b/thanasoft-front/src/examples/Sidenav/SidenavList.vue
@@ -4,298 +4,54 @@
class="w-auto h-auto collapse navbar-collapse max-height-vh-100 h-100"
>
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
+
+
-
+
- GESTION
+ {{ item.text }}
-
-
- -
-
+
-
+
+
+
+
+
+
+
+
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
@@ -304,6 +60,7 @@
+
diff --git a/thanasoft-front/src/router/index.js b/thanasoft-front/src/router/index.js
index 891d8bb..a72549f 100644
--- a/thanasoft-front/src/router/index.js
+++ b/thanasoft-front/src/router/index.js
@@ -522,7 +522,12 @@ const routes = [
{
path: "/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",
@@ -531,7 +536,7 @@ const routes = [
},
{
path: "/employes/vehicules",
- name: "Vehicules",
+ name: "Véhicules",
component: () => import("@/views/pages/Employes/Vehicules.vue"),
},
{
@@ -539,6 +544,12 @@ const routes = [
name: "Absences",
component: () => import("@/views/pages/Employes/Absences.vue"),
},
+ {
+ path: "/employes/thanatopracteurs",
+ name: "Employés Thanatopracteurs",
+ component: () =>
+ import("@/views/pages/Employes/EmployesThanatopracteurs.vue"),
+ },
// Paramétrage
{
path: "/parametrage/droits",
diff --git a/thanasoft-front/src/services/employee.ts b/thanasoft-front/src/services/employee.ts
new file mode 100644
index 0000000..a031dab
--- /dev/null
+++ b/thanasoft-front/src/services/employee.ts
@@ -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 {
+ 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 {
+ const response = await request({
+ url: "/api/employees",
+ method: "get",
+ params,
+ });
+
+ return response;
+ },
+
+ /**
+ * Get a specific employee by ID
+ */
+ async getEmployee(id: number): Promise {
+ const response = await request({
+ url: `/api/employees/${id}`,
+ method: "get",
+ });
+
+ return response;
+ },
+
+ /**
+ * Create a new employee
+ */
+ async createEmployee(
+ payload: CreateEmployeePayload
+ ): Promise {
+ const formattedPayload = this.transformEmployeePayload(payload);
+
+ const response = await request({
+ url: "/api/employees",
+ method: "post",
+ data: formattedPayload,
+ });
+
+ return response;
+ },
+
+ /**
+ * Update an existing employee
+ */
+ async updateEmployee(
+ payload: UpdateEmployeePayload
+ ): Promise {
+ const { id, ...updateData } = payload;
+ const formattedPayload = this.transformEmployeePayload(updateData);
+
+ const response = await request({
+ 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 {
+ const response = await request({
+ 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): 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 {
+ 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 {
+ const response = await request({
+ url: `/api/employees/${id}`,
+ method: "put",
+ data: {
+ active: isActive,
+ },
+ });
+
+ return response;
+ },
+};
+
+export default EmployeeService;
diff --git a/thanasoft-front/src/services/product.ts b/thanasoft-front/src/services/product.ts
index 29b4374..75c18ad 100644
--- a/thanasoft-front/src/services/product.ts
+++ b/thanasoft-front/src/services/product.ts
@@ -5,7 +5,12 @@ export interface Product {
id: number;
nom: string;
reference: string;
- categorie: string;
+ categorie_id: number;
+ category: {
+ id: number;
+ name: string;
+ code: string;
+ };
fabricant: string | null;
stock_actuel: number;
stock_minimum: number;
@@ -41,7 +46,7 @@ export interface Product {
export interface ProductFormData {
nom: string;
reference: string;
- categorie: string;
+ categorie_id: number;
fabricant?: string | null;
stock_actuel: number;
stock_minimum: number;
@@ -88,7 +93,7 @@ class ProductService {
page?: number;
per_page?: number;
search?: string;
- categorie?: string;
+ categorie_id?: number;
fournisseur_id?: number;
low_stock?: boolean;
expiring_soon?: boolean;
@@ -185,7 +190,7 @@ class ProductService {
// Get products by category
async getProductsByCategory(
- category: string,
+ category: number,
params: { per_page?: number } = {}
): Promise {
const response = await request({
diff --git a/thanasoft-front/src/services/productCategorie.ts b/thanasoft-front/src/services/productCategorie.ts
new file mode 100644
index 0000000..fec6ef2
--- /dev/null
+++ b/thanasoft-front/src/services/productCategorie.ts
@@ -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 {
+ const response = await request({
+ 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 {
+ const response = await request({
+ url: "/api/productCategories/low-stock",
+ method: "get",
+ params,
+ });
+ return response;
+ }
+
+ // Get productCategories by category
+ async getProductCategoriesByCategory(
+ category: string,
+ params: { per_page?: number } = {}
+ ): Promise {
+ const response = await request({
+ 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
+ ): 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;
diff --git a/thanasoft-front/src/services/productCategory.ts b/thanasoft-front/src/services/productCategory.ts
new file mode 100644
index 0000000..ead663f
--- /dev/null
+++ b/thanasoft-front/src/services/productCategory.ts
@@ -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 {
+ const response = await request({
+ 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 {
+ const response = await request({
+ 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;
diff --git a/thanasoft-front/src/stores/employeeStore.ts b/thanasoft-front/src/stores/employeeStore.ts
new file mode 100644
index 0000000..44280f3
--- /dev/null
+++ b/thanasoft-front/src/stores/employeeStore.ts
@@ -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([]);
+ const currentEmployee = ref(null);
+ const loading = ref(false);
+ const error = ref(null);
+ const searchResults = ref([]);
+
+ // 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,
+ };
+});
diff --git a/thanasoft-front/src/stores/productCategoryStore.ts b/thanasoft-front/src/stores/productCategoryStore.ts
new file mode 100644
index 0000000..22e6261
--- /dev/null
+++ b/thanasoft-front/src/stores/productCategoryStore.ts
@@ -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([]);
+ const currentProductCategory = ref(null);
+ const loading = ref(false);
+ const error = ref(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;
diff --git a/thanasoft-front/src/stores/productStore.ts b/thanasoft-front/src/stores/productStore.ts
index 94bf2e4..fb4043b 100644
--- a/thanasoft-front/src/stores/productStore.ts
+++ b/thanasoft-front/src/stores/productStore.ts
@@ -1,6 +1,6 @@
import { defineStore } from "pinia";
import ProductService from "@/services/product";
-import type { Product } from "@/services/product";
+import { Product } from "@/services/product";
interface Meta {
current_page: number;
@@ -43,7 +43,7 @@ export const useProductStore = defineStore("product", {
),
categories: (state) => {
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();
},
@@ -212,12 +212,12 @@ export const useProductStore = defineStore("product", {
}
},
- async fetchProductsByCategory(category: string) {
+ async fetchProductsByCategory(categoryId: number) {
this.loading = true;
this.error = null;
try {
- const products = await productService.getProductsByCategory(category);
+ const products = await productService.getProductsByCategory(categoryId);
return products.data;
} catch (error: any) {
this.error =
@@ -269,9 +269,11 @@ export const useProductStore = defineStore("product", {
},
// Local filtering functions
- filterByCategory(category: string) {
- if (!category) return this.products;
- return this.products.filter((product) => product.categorie === category);
+ filterByCategory(categoryId: number) {
+ if (!categoryId) return this.products;
+ return this.products.filter(
+ (product) => product.categorie_id === categoryId
+ );
},
filterByLowStock() {
diff --git a/thanasoft-front/src/views/pages/CRM/AddEmployee.vue b/thanasoft-front/src/views/pages/CRM/AddEmployee.vue
new file mode 100644
index 0000000..fdf4aac
--- /dev/null
+++ b/thanasoft-front/src/views/pages/CRM/AddEmployee.vue
@@ -0,0 +1,59 @@
+
+
+
+
diff --git a/thanasoft-front/src/views/pages/Employes/Employees.vue b/thanasoft-front/src/views/pages/Employes/Employees.vue
new file mode 100644
index 0000000..250d38d
--- /dev/null
+++ b/thanasoft-front/src/views/pages/Employes/Employees.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
diff --git a/thanasoft-front/src/views/pages/Employes/Employes.vue b/thanasoft-front/src/views/pages/Employes/Employes.vue
deleted file mode 100644
index e4cedaf..0000000
--- a/thanasoft-front/src/views/pages/Employes/Employes.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
Gestion employes
-
-
-
-
diff --git a/thanasoft-front/src/views/pages/Employes/EmployesThanatopracteurs.vue b/thanasoft-front/src/views/pages/Employes/EmployesThanatopracteurs.vue
new file mode 100644
index 0000000..6ea115e
--- /dev/null
+++ b/thanasoft-front/src/views/pages/Employes/EmployesThanatopracteurs.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
Module en développement
+
+ Cette section sera bientôt disponible avec toutes les
+ fonctionnalités de gestion des employés et thanatopracticiens.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/thanasoft-front/src/views/pages/Stock/AddProduct.vue b/thanasoft-front/src/views/pages/Stock/AddProduct.vue
index 8db1515..c1fa7b2 100644
--- a/thanasoft-front/src/views/pages/Stock/AddProduct.vue
+++ b/thanasoft-front/src/views/pages/Stock/AddProduct.vue
@@ -4,6 +4,7 @@
:loading="productStore.isLoading"
:validation-errors="validationErrors"
:success="showSuccess"
+ :categories="productCategoryStore.productCategories"
@create-product="handleCreateProduct"
/>
@@ -13,6 +14,7 @@ import AddProductPresentation from "@/components/Organism/Stock/AddProductPresen
import { useFournisseurStore } from "@/stores/fournisseurStore";
import { useProductStore } from "@/stores/productStore";
import { useNotificationStore } from "@/stores/notification";
+import { useProductCategoryStore } from "@/stores/productCategoryStore";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
@@ -20,12 +22,13 @@ const router = useRouter();
const fournisseurStore = useFournisseurStore();
const productStore = useProductStore();
const notificationStore = useNotificationStore();
+const productCategoryStore = useProductCategoryStore();
const validationErrors = ref({});
const showSuccess = ref(false);
onMounted(async () => {
- // Load fournisseurs for the supplier dropdown
await fournisseurStore.fetchFournisseurs();
+ await productCategoryStore.fetchProductCategories();
});
const handleCreateProduct = async (form) => {
diff --git a/thanasoft-front/src/views/pages/Stock/AddProductCategory.vue b/thanasoft-front/src/views/pages/Stock/AddProductCategory.vue
new file mode 100644
index 0000000..48da4ac
--- /dev/null
+++ b/thanasoft-front/src/views/pages/Stock/AddProductCategory.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/thanasoft-front/src/views/pages/Stock/EditProductCategory.vue b/thanasoft-front/src/views/pages/Stock/EditProductCategory.vue
new file mode 100644
index 0000000..3b06360
--- /dev/null
+++ b/thanasoft-front/src/views/pages/Stock/EditProductCategory.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/thanasoft-front/src/views/pages/Stock/ProductCategories.vue b/thanasoft-front/src/views/pages/Stock/ProductCategories.vue
new file mode 100644
index 0000000..b6d92cf
--- /dev/null
+++ b/thanasoft-front/src/views/pages/Stock/ProductCategories.vue
@@ -0,0 +1,42 @@
+
+
+
+
diff --git a/thanasoft-front/src/views/pages/Stock/ProductCategoryDetails.vue b/thanasoft-front/src/views/pages/Stock/ProductCategoryDetails.vue
new file mode 100644
index 0000000..6f39ebe
--- /dev/null
+++ b/thanasoft-front/src/views/pages/Stock/ProductCategoryDetails.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
Catégorie non trouvée
+
+
+
+
+