From e55cc5253e2740f569873ea7cfaaa34c0f2a6685 Mon Sep 17 00:00:00 2001 From: Nyavokevin <42602932+nyavokevin@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:09:12 +0300 Subject: [PATCH] add back: --- th | 88 --- thanas | 611 +++++++++------- thanasoft | 179 +++-- thanasoft-back/API_DOCUMENTATION.md | 656 ++++++++++++++++++ .../Controllers/Api/EmployeeController.php | 272 ++++++++ .../Api/PractitionerDocumentController.php | 320 +++++++++ .../Controllers/Api/ProductController.php | 12 +- .../Api/ThanatopractitionerController.php | 322 +++++++++ .../Http/Requests/StoreEmployeeRequest.php | 59 ++ .../StorePractitionerDocumentRequest.php | 55 ++ .../app/Http/Requests/StoreProductRequest.php | 7 +- .../StoreThanatopractitionerRequest.php | 57 ++ .../Http/Requests/UpdateEmployeeRequest.php | 65 ++ .../UpdatePractitionerDocumentRequest.php | 55 ++ .../Http/Requests/UpdateProductRequest.php | 7 +- .../UpdateThanatopractitionerRequest.php | 62 ++ .../Resources/Employee/EmployeeCollection.php | 43 ++ .../Resources/Employee/EmployeeResource.php | 37 + .../PractitionerDocumentCollection.php | 48 ++ .../Employee/PractitionerDocumentResource.php | 36 + .../ThanatopractitionerCollection.php | 45 ++ .../Employee/ThanatopractitionerResource.php | 41 ++ .../Resources/Product/ProductResource.php | 9 +- thanasoft-back/app/Models/Employee.php | 85 +++ .../app/Models/PractitionerDocument.php | 86 +++ thanasoft-back/app/Models/Product.php | 4 +- thanasoft-back/app/Models/ProductCategory.php | 2 +- .../app/Models/Thanatopractitioner.php | 85 +++ .../app/Providers/AppServiceProvider.php | 13 + .../app/Repositories/EmployeeRepository.php | 127 ++++ .../EmployeeRepositoryInterface.php | 82 +++ .../PractitionerDocumentRepository.php | 149 ++++ ...ractitionerDocumentRepositoryInterface.php | 75 ++ .../app/Repositories/ProductRepository.php | 15 +- .../Repositories/ProductRepository.php -
-
- {{ label }} - {{ value }} {{ unit }} -
-
- Min: {{ threshold }} {{ unit }} -
-
- - - - - \ No newline at end of file diff --git a/thanas b/thanas index 90216eb..eab35fa 100644 --- a/thanas +++ b/thanas @@ -1,259 +1,370 @@ import { defineStore } from "pinia"; -import ProductService from "@/services/product"; +import { ref, computed } from "vue"; +import EmployeeService from "@/services/employee"; -export const useProductStore = defineStore("product", { - state: () => ({ - products: [], - currentProduct: null, - loading: false, - isLoading: false, - error: null, - meta: { +import type { + Employee, + CreateEmployeePayload, + UpdateEmployeePayload, + EmployeeListResponse, +} 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.is_active) + ); + const inactiveEmployees = computed(() => + employees.value.filter((employee) => !employee.is_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.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); + setPagination(response.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.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.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.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: 15, + per_page: 10, total: 0, - from: 1, + from: 0, to: 0, - }, - }), + }; + }; - getters: { - lowStockProducts: (state) => - state.products.filter((product) => product.is_low_stock), - expiringProducts: (state) => - state.products.filter((product) => - product.date_expiration && - new Date(product.date_expiration) <= new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) - ), - categories: (state) => { - const categorySet = new Set(state.products.map(product => product.categorie).filter(Boolean)); - return Array.from(categorySet).sort(); - }, - totalProducts: (state) => state.meta.total, - totalValue: (state) => - state.products.reduce((total, product) => - total + (product.stock_actuel * product.prix_unitaire), 0 - ), - }, + return { + // State + employees, + currentEmployee, + loading, + error, + searchResults, - actions: { - async fetchProducts(params = {}) { - this.loading = true; - this.error = null; - - try { - const response = await ProductService.getAllProducts(params); - - this.products = response.data; - this.meta = { - current_page: response.pagination.current_page, - last_page: response.pagination.last_page, - per_page: response.pagination.per_page, - total: response.pagination.total, - from: response.pagination.from, - to: response.pagination.to, - }; - - return response; - } catch (error: any) { - this.error = error?.message || "Erreur lors du chargement des produits"; - throw error; - } finally { - this.loading = false; - } - }, + // Getters + allEmployees, + activeEmployees, + inactiveEmployees, + isLoading, + hasError, + getError, + getEmployeeById, + getPagination, - async createProduct(productData: any) { - this.isLoading = true; - this.error = null; - - try { - const response = await ProductService.createProduct(productData); - const product = response.data; - - // Add the new product to the beginning of the list - this.products.unshift(product); - this.meta.total += 1; - - return product; - } catch (error: any) { - this.error = error?.message || "Erreur lors de la création du produit"; - throw error; - } finally { - this.isLoading = false; - } - }, - - async updateProduct(id: number, productData: any) { - this.isLoading = true; - this.error = null; - - try { - const response = await ProductService.updateProduct(id, productData); - const updatedProduct = response.data; - - // Update the product in the list - const index = this.products.findIndex((p) => p.id === id); - if (index !== -1) { - this.products[index] = updatedProduct; - } - - // Update current product if it matches - if (this.currentProduct?.id === id) { - this.currentProduct = updatedProduct; - } - - return updatedProduct; - } catch (error: any) { - this.error = error?.message || "Erreur lors de la mise à jour du produit"; - throw error; - } finally { - this.isLoading = false; - } - }, - - async deleteProduct(id: number) { - this.isLoading = true; - this.error = null; - - try { - await ProductService.deleteProduct(id); - - // Remove the product from the list - this.products = this.products.filter((p) => p.id !== id); - this.meta.total -= 1; - - // Clear current product if it was deleted - if (this.currentProduct?.id === id) { - this.currentProduct = null; - } - - return true; - } catch (error: any) { - this.error = error?.message || "Erreur lors de la suppression du produit"; - throw error; - } finally { - this.isLoading = false; - } - }, - - async fetchProduct(id: number) { - this.loading = true; - this.error = null; - - try { - const response = await ProductService.getProduct(id); - this.currentProduct = response.data; - return response.data; - } catch (error: any) { - this.error = error?.message || "Erreur lors du chargement du produit"; - throw error; - } finally { - this.loading = false; - } - }, - - async searchProducts(searchTerm: string, exact = false) { - this.loading = true; - this.error = null; - - try { - const response = await ProductService.searchProducts(searchTerm, exact); - - // Update current products list with search results - this.products = response.data; - - return response.data; - } catch (error: any) { - this.error = error?.message || "Erreur lors de la recherche"; - throw error; - } finally { - this.loading = false; - } - }, - - async fetchLowStockProducts() { - this.loading = true; - this.error = null; - - try { - const response = await ProductService.getLowStockProducts(); - return response; - } catch (error: any) { - this.error = error?.message || "Erreur lors du chargement des produits à stock faible"; - throw error; - } finally { - this.loading = false; - } - }, - - async fetchProductsByCategory(category: string) { - this.loading = true; - this.error = null; - - try { - const response = await ProductService.getProductsByCategory(category); - return response; - } catch (error: any) { - this.error = error?.message || "Erreur lors du chargement des produits par catégorie"; - throw error; - } finally { - this.loading = false; - } - }, - - async getProductStatistics() { - try { - const response = await ProductService.getProductStatistics(); - return response.data; - } catch (error: any) { - this.error = error?.message || "Erreur lors du chargement des statistiques"; - throw error; - } - }, - - async updateStock(productId: number, newStock: number) { - try { - const response = await ProductService.updateStock(productId, newStock); - - // Update the product in the list - const index = this.products.findIndex((p) => p.id === productId); - if (index !== -1) { - this.products[index] = response.data; - } - - return response.data; - } catch (error: any) { - this.error = error?.message || "Erreur lors de la mise à jour du stock"; - throw error; - } - }, - - resetState() { - this.products = []; - this.currentProduct = null; - this.error = null; - this.loading = false; - this.isLoading = false; - }, - - // Local filtering functions - filterByCategory(category: string) { - if (!category) return this.products; - return this.products.filter((product: any) => product.categorie === category); - }, - - filterByLowStock() { - return this.products.filter((product: any) => product.is_low_stock); - }, - - filterByExpiration(days = 30) { - const cutoffDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000); - return this.products.filter((product: any) => - product.date_expiration && - new Date(product.date_expiration) <= cutoffDate - ); - }, - }, -}); \ No newline at end of file + // Actions + fetchEmployees, + fetchEmployee, + createEmployee, + updateEmployee, + deleteEmployee, + searchEmployees, + toggleEmployeeStatus, + fetchStatistics, + clearCurrentEmployee, + clearStore, + clearError, + }; +}); diff --git a/thanasoft b/thanasoft index dd188ed..9693b75 100644 --- a/thanasoft +++ b/thanasoft @@ -1,64 +1,141 @@ \ No newline at end of file + .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; + } + } + diff --git a/thanasoft-back/API_DOCUMENTATION.md b/thanasoft-back/API_DOCUMENTATION.md new file mode 100644 index 0000000..e9d846e --- /dev/null +++ b/thanasoft-back/API_DOCUMENTATION.md @@ -0,0 +1,656 @@ +# API Documentation - Employee Management System + +## Overview + +The ThanaSoft Employee Management System provides comprehensive RESTful API endpoints for managing employees, thanatopractitioners, and their associated documents. This system is built using Laravel and follows RESTful API conventions. + +## Base URL + +``` +https://your-domain.com/api +``` + +## Authentication + +All API endpoints require authentication using Laravel Sanctum. Include the token in the Authorization header: + +``` +Authorization: Bearer {your_token} +``` + +## Employee Management System + +### Entities + +1. **Employees** - Core employee records with personal information +2. **Thanatopractitioners** - Specialized practitioners linked to employees +3. **Practitioner Documents** - Documents associated with thanatopractitioners + +--- + +## Employees API + +### Base Endpoint + +``` +/employees +``` + +### Endpoints Overview + +| Method | Endpoint | Description | +| ------ | --------------------------------- | ----------------------------------------------- | +| GET | `/employees` | List all employees with pagination | +| POST | `/employees` | Create a new employee | +| GET | `/employees/{id}` | Get specific employee details | +| PUT | `/employees/{id}` | Update employee information | +| DELETE | `/employees/{id}` | Delete an employee | +| GET | `/employees/searchBy` | Search employees by criteria | +| GET | `/employees/thanatopractitioners` | Get all thanatopractitioners with employee info | + +### 1. List All Employees + +**GET** `/api/employees` + +**Query Parameters:** + +- `page` (optional): Page number for pagination (default: 1) +- `per_page` (optional): Items per page (default: 15, max: 100) +- `search` (optional): Search term for name, email, or employee_number +- `department` (optional): Filter by department +- `status` (optional): Filter by employment status (active, inactive, terminated) + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2020-01-15", + "status": "active", + "created_at": "2025-11-05T10:30:00.000000Z", + "updated_at": "2025-11-05T10:30:00.000000Z" + } + ], + "current_page": 1, + "per_page": 15, + "total": 25, + "last_page": 2 + }, + "message": "Employés récupérés avec succès" +} +``` + +### 2. Create Employee + +**POST** `/api/employees` + +**Request Body:** + +```json +{ + "employee_number": "EMP026", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "address": "123 Rue de la Liberté, Antananarivo", + "birth_date": "1985-06-15", + "gender": "male", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2025-11-05", + "salary": 2500000, + "status": "active" +} +``` + +**Success Response (201):** + +```json +{ + "success": true, + "data": { + "id": 26, + "employee_number": "EMP026", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "address": "123 Rue de la Liberté, Antananarivo", + "birth_date": "1985-06-15", + "gender": "male", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2025-11-05", + "salary": 2500000, + "status": "active", + "created_at": "2025-11-05T12:16:05.000000Z", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Employé créé avec succès" +} +``` + +**Validation Errors (422):** + +```json +{ + "success": false, + "message": "Les données fournies ne sont pas valides", + "errors": { + "email": ["L'adresse email doit être unique"], + "employee_number": ["Le numéro d'employé est requis"] + } +} +``` + +### 3. Get Employee Details + +**GET** `/api/employees/{id}` + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 67", + "address": "123 Rue de la Liberté, Antananarivo", + "birth_date": "1985-06-15", + "gender": "male", + "department": "Direction", + "position": "Directeur Général", + "hire_date": "2020-01-15", + "salary": 2500000, + "status": "active", + "created_at": "2020-01-15T08:00:00.000000Z", + "updated_at": "2025-11-05T10:30:00.000000Z" + }, + "message": "Détails de l'employé récupérés avec succès" +} +``` + +### 4. Update Employee + +**PUT** `/api/employees/{id}` + +**Request Body:** Same as create, but all fields are optional. + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "phone": "+261 34 12 345 68", + "department": "Direction", + "position": "Directeur Général Adjoint", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Employé mis à jour avec succès" +} +``` + +### 5. Search Employees + +**GET** `/api/employees/searchBy` + +**Query Parameters:** + +- `query` (required): Search term +- `field` (optional): Field to search in (first_name, last_name, email, employee_number, department, position) + +**Example:** `/api/employees/searchBy?query=jean&field=first_name` + +**Success Response (200):** + +```json +{ + "success": true, + "data": [ + { + "id": 1, + "employee_number": "EMP001", + "first_name": "Jean", + "last_name": "Dupont", + "email": "jean.dupont@thanasoft.com", + "department": "Direction", + "position": "Directeur Général" + } + ], + "message": "Résultats de recherche récupérés avec succès" +} +``` + +--- + +## Thanatopractitioners API + +### Base Endpoint + +``` +/thanatopractitioners +``` + +### Endpoints Overview + +| Method | Endpoint | Description | +| ------ | ---------------------------------------------- | ------------------------------------ | +| GET | `/thanatopractitioners` | List all thanatopractitioners | +| POST | `/thanatopractitioners` | Create a new thanatopractitioner | +| GET | `/thanatopractitioners/{id}` | Get specific thanatopractitioner | +| PUT | `/thanatopractitioners/{id}` | Update thanatopractitioner | +| DELETE | `/thanatopractitioners/{id}` | Delete thanatopractitioner | +| GET | `/employees/{employeeId}/thanatopractitioners` | Get thanatopractitioners by employee | + +### 1. List All Thanatopractitioners + +**GET** `/api/thanatopractitioners` + +**Query Parameters:** + +- `page` (optional): Page number +- `per_page` (optional): Items per page +- `specialization` (optional): Filter by specialization + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": 1, + "employee_id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2020-01-15", + "authorization_expiry": "2025-01-15", + "is_authorized": true, + "created_at": "2020-01-15T08:00:00.000000Z", + "updated_at": "2025-11-05T10:30:00.000000Z", + "employee": { + "id": 1, + "first_name": "Jean", + "last_name": "Dupont", + "employee_number": "EMP001", + "department": "Direction" + } + } + ], + "current_page": 1, + "per_page": 15, + "total": 5, + "last_page": 1 + }, + "message": "Thanatopraticiens récupérés avec succès" +} +``` + +### 2. Create Thanatopractitioner + +**POST** `/api/thanatopractitioners` + +**Request Body:** + +```json +{ + "employee_id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2025-11-05", + "authorization_expiry": "2026-11-05", + "is_authorized": true +} +``` + +**Success Response (201):** + +```json +{ + "success": true, + "data": { + "id": 6, + "employee_id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2025-11-05", + "authorization_expiry": "2026-11-05", + "is_authorized": true, + "created_at": "2025-11-05T12:16:05.000000Z", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Thanatopraticien créé avec succès" +} +``` + +### 3. Get Thanatopractitioners by Employee + +**GET** `/api/employees/{employeeId}/thanatopractitioners` + +**Success Response (200):** + +```json +{ + "success": true, + "data": [ + { + "id": 1, + "specialization": "Thanatopraticien principal", + "license_number": "TH001", + "authorization_number": "AUTH001", + "authorization_date": "2020-01-15", + "authorization_expiry": "2025-01-15", + "is_authorized": true + } + ], + "message": "Thanatopraticiens de l'employé récupérés avec succès" +} +``` + +--- + +## Practitioner Documents API + +### Base Endpoint + +``` +/practitioner-documents +``` + +### Endpoints Overview + +| Method | Endpoint | Description | +| ------ | -------------------------------------- | ------------------------------------ | +| GET | `/practitioner-documents` | List all documents | +| POST | `/practitioner-documents` | Upload new document | +| GET | `/practitioner-documents/{id}` | Get specific document | +| PUT | `/practitioner-documents/{id}` | Update document info | +| DELETE | `/practitioner-documents/{id}` | Delete document | +| GET | `/practitioner-documents/searchBy` | Search documents | +| GET | `/practitioner-documents/expiring` | Get expiring documents | +| GET | `/thanatopractitioners/{id}/documents` | Get documents by thanatopractitioner | +| PATCH | `/practitioner-documents/{id}/verify` | Verify document | + +### 1. List All Documents + +**GET** `/api/practitioner-documents` + +**Query Parameters:** + +- `page` (optional): Page number +- `per_page` (optional): Items per page +- `type` (optional): Filter by document type +- `is_verified` (optional): Filter by verification status + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": 1, + "thanatopractitioner_id": 1, + "document_type": "diplome", + "file_name": "diplome_thanatopraticien.pdf", + "file_path": "/documents/diplome_thanatopraticien.pdf", + "issue_date": "2019-06-30", + "expiry_date": "2024-06-30", + "issuing_authority": "Ministère de la Santé", + "is_verified": true, + "verified_at": "2020-01-20T10:00:00.000000Z", + "created_at": "2020-01-15T08:00:00.000000Z", + "updated_at": "2020-01-20T10:00:00.000000Z" + } + ], + "current_page": 1, + "per_page": 15, + "total": 12, + "last_page": 1 + }, + "message": "Documents récupérés avec succès" +} +``` + +### 2. Upload Document + +**POST** `/api/practitioner-documents` + +**Form Data:** + +``` +thanatopractitioner_id: 1 +document_type: diplome +file: [binary file data] +issue_date: 2019-06-30 +expiry_date: 2024-06-30 +issuing_authority: Ministère de la Santé +``` + +**Success Response (201):** + +```json +{ + "success": true, + "data": { + "id": 13, + "thanatopractitioner_id": 1, + "document_type": "diplome", + "file_name": "diplome_thanatopraticien_2025.pdf", + "file_path": "/documents/diplome_thanatopraticien_2025.pdf", + "issue_date": "2019-06-30", + "expiry_date": "2024-06-30", + "issuing_authority": "Ministère de la Santé", + "is_verified": false, + "created_at": "2025-11-05T12:16:05.000000Z", + "updated_at": "2025-11-05T12:16:05.000000Z" + }, + "message": "Document téléchargé avec succès" +} +``` + +### 3. Get Expiring Documents + +**GET** `/api/practitioner-documents/expiring` + +**Query Parameters:** + +- `days` (optional): Number of days to look ahead (default: 30) + +**Success Response (200):** + +```json +{ + "success": true, + "data": [ + { + "id": 5, + "thanatopractitioner_id": 2, + "document_type": "certificat", + "file_name": "certificat_renouvellement.pdf", + "expiry_date": "2025-11-20", + "days_until_expiry": 15, + "employee": { + "first_name": "Marie", + "last_name": "Rasoa", + "employee_number": "EMP002" + } + } + ], + "message": "Documents expirants récupérés avec succès" +} +``` + +### 4. Verify Document + +**PATCH** `/api/practitioner-documents/{id}/verify` + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "id": 13, + "document_type": "diplome", + "is_verified": true, + "verified_at": "2025-11-05T12:20:00.000000Z" + }, + "message": "Document vérifié avec succès" +} +``` + +--- + +## Data Models + +### Employee Model + +| Field | Type | Required | Description | +| ----------------- | ------- | -------- | ------------------------------------------------ | +| `employee_number` | string | Yes | Unique employee identifier | +| `first_name` | string | Yes | Employee's first name | +| `last_name` | string | Yes | Employee's last name | +| `email` | email | Yes | Unique email address | +| `phone` | string | No | Phone number | +| `address` | text | No | Physical address | +| `birth_date` | date | No | Date of birth | +| `gender` | string | No | Gender (male, female, other) | +| `department` | string | No | Department name | +| `position` | string | No | Job position | +| `hire_date` | date | No | Employment start date | +| `salary` | decimal | No | Monthly salary | +| `status` | string | No | Employment status (active, inactive, terminated) | + +### Thanatopractitioner Model + +| Field | Type | Required | Description | +| ---------------------- | ------- | -------- | ------------------------------ | +| `employee_id` | integer | Yes | Foreign key to employees table | +| `specialization` | string | Yes | Area of specialization | +| `license_number` | string | Yes | Professional license number | +| `authorization_number` | string | Yes | Authorization number | +| `authorization_date` | date | Yes | Authorization issue date | +| `authorization_expiry` | date | Yes | Authorization expiry date | +| `is_authorized` | boolean | Yes | Authorization status | + +### Practitioner Document Model + +| Field | Type | Required | Description | +| ------------------------ | --------- | -------- | ----------------------------------------- | +| `thanatopractitioner_id` | integer | Yes | Foreign key to thanatopractitioners table | +| `document_type` | string | Yes | Type of document | +| `file` | file | Yes | Document file upload | +| `issue_date` | date | No | Document issue date | +| `expiry_date` | date | No | Document expiry date | +| `issuing_authority` | string | No | Authority that issued the document | +| `is_verified` | boolean | Yes | Verification status | +| `verified_at` | timestamp | No | Verification timestamp | + +--- + +## Error Handling + +### HTTP Status Codes + +- `200` - Success +- `201` - Created successfully +- `400` - Bad Request +- `401` - Unauthorized +- `403` - Forbidden +- `404` - Resource not found +- `422` - Validation error +- `500` - Internal server error + +### Error Response Format + +```json +{ + "success": false, + "message": "Description of the error", + "errors": { + "field_name": ["Error message for this field"] + } +} +``` + +--- + +## File Upload + +For document uploads, use multipart/form-data: + +``` +POST /api/practitioner-documents +Content-Type: multipart/form-data + +{ + "thanatopractitioner_id": 1, + "document_type": "diplome", + "file": [binary file], + "issue_date": "2019-06-30", + "expiry_date": "2024-06-30", + "issuing_authority": "Ministère de la Santé" +} +``` + +--- + +## Pagination + +All list endpoints support pagination with the following query parameters: + +- `page`: Page number (default: 1) +- `per_page`: Items per page (default: 15, max: 100) + +Response includes pagination metadata: + +```json +{ + "current_page": 1, + "per_page": 15, + "total": 50, + "last_page": 4, + "from": 1, + "to": 15 +} +``` + +--- + +## Rate Limiting + +API requests are rate limited to 1000 requests per hour per authenticated user. Exceeding this limit will result in a `429 Too Many Requests` response. + +--- + +## Support + +For API support or questions, please contact the development team or refer to the Laravel documentation at https://laravel.com/docs. diff --git a/thanasoft-back/app/Http/Controllers/Api/EmployeeController.php b/thanasoft-back/app/Http/Controllers/Api/EmployeeController.php new file mode 100644 index 0000000..8d675b2 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/EmployeeController.php @@ -0,0 +1,272 @@ +get('per_page', 15); + + $filters = [ + 'search' => $request->get('search'), + 'active' => $request->get('active'), + 'sort_by' => $request->get('sort_by', 'last_name'), + 'sort_direction' => $request->get('sort_direction', 'asc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $result = $this->employeeRepository->getPaginated($perPage, $filters); + + return response()->json([ + 'data' => new EmployeeCollection($result['employees']), + 'pagination' => $result['pagination'], + 'message' => 'Employés récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching employees: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display paginated employees. + */ + public function paginated(Request $request): JsonResponse + { + try { + $perPage = (int) $request->get('per_page', 15); + $result = $this->employeeRepository->getPaginated($perPage, []); + + return response()->json([ + 'data' => new EmployeeCollection($result['employees']), + 'pagination' => $result['pagination'], + 'message' => 'Employés récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching paginated employees: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get active employees only. + */ + public function active(): EmployeeCollection|JsonResponse + { + try { + $employees = $this->employeeRepository->getActive(); + return new EmployeeCollection($employees); + } catch (\Exception $e) { + Log::error('Error fetching active employees: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés actifs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get employees with thanatopractitioner data. + */ + public function withThanatopractitioner(): EmployeeCollection|JsonResponse + { + try { + $employees = $this->employeeRepository->getWithThanatopractitioner(); + return new EmployeeCollection($employees); + } catch (\Exception $e) { + Log::error('Error fetching employees with thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des employés avec données thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get employee statistics. + */ + public function statistics(): JsonResponse + { + try { + $statistics = $this->employeeRepository->getStatistics(); + + return response()->json([ + 'data' => $statistics, + 'message' => 'Statistiques des employés récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching employee statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created employee. + */ + public function store(StoreEmployeeRequest $request): EmployeeResource|JsonResponse + { + try { + $employee = $this->employeeRepository->create($request->validated()); + return new EmployeeResource($employee); + } catch (\Exception $e) { + Log::error('Error creating employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified employee. + */ + public function show(string $id): EmployeeResource|JsonResponse + { + try { + $employee = $this->employeeRepository->find($id); + + if (!$employee) { + return response()->json([ + 'message' => 'Employé non trouvé.', + ], 404); + } + + return new EmployeeResource($employee); + } catch (\Exception $e) { + Log::error('Error fetching employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified employee. + */ + public function update(UpdateEmployeeRequest $request, string $id): EmployeeResource|JsonResponse + { + try { + $updated = $this->employeeRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Employé non trouvé ou échec de la mise à jour.', + ], 404); + } + + $employee = $this->employeeRepository->find($id); + return new EmployeeResource($employee); + } catch (\Exception $e) { + Log::error('Error updating employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified employee. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->employeeRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Employé non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Employé supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'employé.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/PractitionerDocumentController.php b/thanasoft-back/app/Http/Controllers/Api/PractitionerDocumentController.php new file mode 100644 index 0000000..eb0e1f3 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/PractitionerDocumentController.php @@ -0,0 +1,320 @@ + $request->get('search'), + 'practitioner_id' => $request->get('practitioner_id'), + 'doc_type' => $request->get('doc_type'), + 'valid_only' => $request->get('valid_only'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $documents = $this->practitionerDocumentRepository->getAll($filters); + + return new PractitionerDocumentCollection($documents); + + } catch (\Exception $e) { + Log::error('Error fetching practitioner documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents des praticiens.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display paginated practitioner documents. + */ + public function paginated(Request $request): JsonResponse + { + try { + $perPage = $request->get('per_page', 15); + $result = $this->practitionerDocumentRepository->getPaginated($perPage); + + return response()->json([ + 'data' => new PractitionerDocumentCollection($result['documents']), + 'pagination' => $result['pagination'], + 'message' => 'Documents des praticiens récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching paginated practitioner documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents des praticiens.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by practitioner ID. + */ + public function byPractitioner(string $practitionerId): PractitionerDocumentCollection|JsonResponse + { + try { + $documents = $this->practitionerDocumentRepository->getByPractitionerId((int) $practitionerId); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching documents by practitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'practitioner_id' => $practitionerId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get documents by type. + */ + public function byType(Request $request): PractitionerDocumentCollection|JsonResponse + { + try { + $docType = $request->get('doc_type'); + + if (!$docType) { + return response()->json([ + 'message' => 'Le paramètre doc_type est requis.', + ], 400); + } + + $documents = $this->practitionerDocumentRepository->getByDocumentType($docType); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching documents by type: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'doc_type' => $docType, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents par type.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get valid documents (not expired). + */ + public function valid(): PractitionerDocumentCollection|JsonResponse + { + try { + $documents = $this->practitionerDocumentRepository->getValid(); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching valid documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents valides.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get expired documents. + */ + public function expired(): PractitionerDocumentCollection|JsonResponse + { + try { + $documents = $this->practitionerDocumentRepository->getExpired(); + return new PractitionerDocumentCollection($documents); + } catch (\Exception $e) { + Log::error('Error fetching expired documents: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des documents expirés.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get practitioner document statistics. + */ + public function statistics(): JsonResponse + { + try { + $statistics = $this->practitionerDocumentRepository->getStatistics(); + + return response()->json([ + 'data' => $statistics, + 'message' => 'Statistiques des documents des praticiens récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching practitioner document statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created practitioner document. + */ + public function store(StorePractitionerDocumentRequest $request): PractitionerDocumentResource|JsonResponse + { + try { + $document = $this->practitionerDocumentRepository->create($request->validated()); + return new PractitionerDocumentResource($document); + } catch (\Exception $e) { + Log::error('Error creating practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified practitioner document. + */ + public function show(string $id): PractitionerDocumentResource|JsonResponse + { + try { + $document = $this->practitionerDocumentRepository->find($id); + + if (!$document) { + return response()->json([ + 'message' => 'Document du praticien non trouvé.', + ], 404); + } + + return new PractitionerDocumentResource($document); + } catch (\Exception $e) { + Log::error('Error fetching practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified practitioner document. + */ + public function update(UpdatePractitionerDocumentRequest $request, string $id): PractitionerDocumentResource|JsonResponse + { + try { + $updated = $this->practitionerDocumentRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Document du praticien non trouvé ou échec de la mise à jour.', + ], 404); + } + + $document = $this->practitionerDocumentRepository->find($id); + return new PractitionerDocumentResource($document); + } catch (\Exception $e) { + Log::error('Error updating practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified practitioner document. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->practitionerDocumentRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Document du praticien non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Document du praticien supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting practitioner document: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'document_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du document du praticien.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ProductController.php b/thanasoft-back/app/Http/Controllers/Api/ProductController.php index f476f89..f16e6da 100644 --- a/thanasoft-back/app/Http/Controllers/Api/ProductController.php +++ b/thanasoft-back/app/Http/Controllers/Api/ProductController.php @@ -31,7 +31,7 @@ class ProductController extends Controller $perPage = $request->get('per_page', 15); $filters = [ 'search' => $request->get('search'), - 'categorie' => $request->get('categorie'), + 'categorie' => $request->get('categorie_id'), 'fournisseur_id' => $request->get('fournisseur_id'), 'low_stock' => $request->get('low_stock'), 'expiring_soon' => $request->get('expiring_soon'), @@ -196,16 +196,16 @@ class ProductController extends Controller public function byCategory(Request $request): ProductCollection|JsonResponse { try { - $category = $request->get('category'); + $categoryId = $request->get('category_id'); $perPage = $request->get('per_page', 15); - if (empty($category)) { + if (empty($categoryId)) { return response()->json([ - 'message' => 'Le paramètre "category" est requis.', + 'message' => 'Le paramètre "category_id" est requis.', ], 400); } - $products = $this->productRepository->getByCategory($category, $perPage); + $products = $this->productRepository->getByCategory($categoryId, $perPage); return new ProductCollection($products); @@ -213,7 +213,7 @@ class ProductController extends Controller Log::error('Error fetching products by category: ' . $e->getMessage(), [ 'exception' => $e, 'trace' => $e->getTraceAsString(), - 'category' => $category, + 'category_id' => $categoryId, ]); return response()->json([ diff --git a/thanasoft-back/app/Http/Controllers/Api/ThanatopractitionerController.php b/thanasoft-back/app/Http/Controllers/Api/ThanatopractitionerController.php new file mode 100644 index 0000000..2b6f8ef --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ThanatopractitionerController.php @@ -0,0 +1,322 @@ +get('per_page', 15); + + $filters = [ + 'search' => $request->get('search'), + 'valid_authorization' => $request->get('valid_authorization'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $result = $this->thanatopractitionerRepository->getPaginated($perPage, $filters); + + return response()->json([ + 'data' => new ThanatopractitionerCollection($result['thanatopractitioners']), + 'pagination' => $result['pagination'], + 'message' => 'Thanatopractitioners récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display paginated thanatopractitioners. + */ + public function paginated(Request $request): JsonResponse + { + try { + $perPage = (int) $request->get('per_page', 15); + $result = $this->thanatopractitionerRepository->getPaginated($perPage, []); + + return response()->json([ + 'data' => new ThanatopractitionerCollection($result['thanatopractitioners']), + 'pagination' => $result['pagination'], + 'message' => 'Thanatopractitioners récupérés avec succès.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error fetching paginated thanatopractitioners: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioners with valid authorization. + */ + public function withValidAuthorization(): ThanatopractitionerCollection|JsonResponse + { + try { + $thanatopractitioners = $this->thanatopractitionerRepository->getWithValidAuthorization(); + return new ThanatopractitionerCollection($thanatopractitioners); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners with valid authorization: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners avec autorisation valide.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioners with expired authorization. + */ + public function withExpiredAuthorization(): ThanatopractitionerCollection|JsonResponse + { + try { + $thanatopractitioners = $this->thanatopractitionerRepository->getWithExpiredAuthorization(); + return new ThanatopractitionerCollection($thanatopractitioners); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners with expired authorization: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners avec autorisation expirée.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioners with their complete data. + */ + public function withRelations(): ThanatopractitionerCollection|JsonResponse + { + try { + $thanatopractitioners = $this->thanatopractitionerRepository->getWithRelations(); + return new ThanatopractitionerCollection($thanatopractitioners); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioners with relations: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des thanatopractitioners avec relations.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Get thanatopractitioner statistics. + */ + public function statistics(): JsonResponse + { + try { + $statistics = $this->thanatopractitionerRepository->getStatistics(); + + return response()->json([ + 'data' => $statistics, + 'message' => 'Statistiques des thanatopractitioners récupérées avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioner statistics: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des statistiques.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created thanatopractitioner. + */ + public function store(StoreThanatopractitionerRequest $request): ThanatopractitionerResource|JsonResponse + { + try { + $thanatopractitioner = $this->thanatopractitionerRepository->create($request->validated()); + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error creating thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified thanatopractitioner. + */ + public function show(string $id): ThanatopractitionerResource|JsonResponse + { + try { + $thanatopractitioner = $this->thanatopractitionerRepository->find($id); + + if (!$thanatopractitioner) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé.', + ], 404); + } + + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'thanatopractitioner_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Find a thanatopractitioner by employee ID. + */ + public function findByEmployee(string $employeeId): ThanatopractitionerResource|JsonResponse + { + try { + $thanatopractitioner = $this->thanatopractitionerRepository->findByEmployeeId((int) $employeeId); + + if (!$thanatopractitioner) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé pour cet employé.', + ], 404); + } + + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error fetching thanatopractitioner by employee: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'employee_id' => $employeeId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified thanatopractitioner. + */ + public function update(UpdateThanatopractitionerRequest $request, string $id): ThanatopractitionerResource|JsonResponse + { + try { + $updated = $this->thanatopractitionerRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé ou échec de la mise à jour.', + ], 404); + } + + $thanatopractitioner = $this->thanatopractitionerRepository->find($id); + return new ThanatopractitionerResource($thanatopractitioner); + } catch (\Exception $e) { + Log::error('Error updating thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'thanatopractitioner_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified thanatopractitioner. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->thanatopractitionerRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Thanatopractitioner non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Thanatopractitioner supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting thanatopractitioner: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'thanatopractitioner_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du thanatopractitioner.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreEmployeeRequest.php b/thanasoft-back/app/Http/Requests/StoreEmployeeRequest.php new file mode 100644 index 0000000..93fc0b1 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreEmployeeRequest.php @@ -0,0 +1,59 @@ + + */ + public function rules(): array + { + return [ + 'first_name' => 'required|string|max:191', + 'last_name' => 'required|string|max:191', + 'email' => 'nullable|email|max:191|unique:employees,email', + 'phone' => 'nullable|string|max:50', + 'job_title' => 'nullable|string|max:191', + 'hire_date' => 'nullable|date', + 'active' => 'boolean', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'first_name.required' => 'Le prénom est obligatoire.', + 'first_name.string' => 'Le prénom doit être une chaîne de caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser :max caractères.', + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.string' => 'Le nom de famille doit être une chaîne de caractères.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser :max caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.unique' => 'Cette adresse email est déjà utilisée.', + 'phone.string' => 'Le téléphone doit être une chaîne de caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser :max caractères.', + 'job_title.string' => 'L\'intitulé du poste doit être une chaîne de caractères.', + 'job_title.max' => 'L\'intitulé du poste ne peut pas dépasser :max caractères.', + 'hire_date.date' => 'La date d\'embauche doit être une date valide.', + 'active.boolean' => 'Le statut actif doit être un booléen.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StorePractitionerDocumentRequest.php b/thanasoft-back/app/Http/Requests/StorePractitionerDocumentRequest.php new file mode 100644 index 0000000..2f18457 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StorePractitionerDocumentRequest.php @@ -0,0 +1,55 @@ + + */ + public function rules(): array + { + return [ + 'practitioner_id' => 'required|exists:thanatopractitioners,id', + 'doc_type' => 'required|string|max:191', + 'file_id' => 'nullable|exists:files,id', + 'issue_date' => 'nullable|date', + 'expiry_date' => 'nullable|date|after_or_equal:issue_date', + 'status' => 'nullable|string|max:64', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'practitioner_id.required' => 'Le thanatopractitioner est obligatoire.', + 'practitioner_id.exists' => 'Le thanatopractitioner sélectionné n\'existe pas.', + 'doc_type.required' => 'Le type de document est obligatoire.', + 'doc_type.string' => 'Le type de document doit être une chaîne de caractères.', + 'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.', + 'file_id.exists' => 'Le fichier sélectionné n\'existe pas.', + 'issue_date.date' => 'La date de délivrance doit être une date valide.', + 'expiry_date.date' => 'La date d\'expiration doit être une date valide.', + 'expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'status.string' => 'Le statut doit être une chaîne de caractères.', + 'status.max' => 'Le statut ne peut pas dépasser :max caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreProductRequest.php b/thanasoft-back/app/Http/Requests/StoreProductRequest.php index 2936556..1e293cb 100644 --- a/thanasoft-back/app/Http/Requests/StoreProductRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreProductRequest.php @@ -24,7 +24,7 @@ class StoreProductRequest extends FormRequest return [ 'nom' => 'required|string|max:255', 'reference' => 'required|string|max:100|unique:products,reference', - 'categorie' => 'required|string|max:191', + 'categorie_id' => 'required|exists:product_categories,id', 'fabricant' => 'nullable|string|max:191', 'stock_actuel' => 'required|numeric|min:0', 'stock_minimum' => 'required|numeric|min:0', @@ -52,9 +52,8 @@ class StoreProductRequest extends FormRequest 'reference.string' => 'La référence du produit doit être une chaîne de caractères.', 'reference.max' => 'La référence du produit ne peut pas dépasser 100 caractères.', 'reference.unique' => 'Cette référence de produit existe déjà.', - 'categorie.required' => 'La catégorie est obligatoire.', - 'categorie.string' => 'La catégorie doit être une chaîne de caractères.', - 'categorie.max' => 'La catégorie ne peut pas dépasser 191 caractères.', + 'categorie_id.required' => 'La catégorie est obligatoire.', + 'categorie_id.exists' => 'La catégorie sélectionnée n\'existe pas.', 'fabricant.string' => 'Le fabricant doit être une chaîne de caractères.', 'fabricant.max' => 'Le fabricant ne peut pas dépasser 191 caractères.', 'stock_actuel.required' => 'Le stock actuel est obligatoire.', diff --git a/thanasoft-back/app/Http/Requests/StoreThanatopractitionerRequest.php b/thanasoft-back/app/Http/Requests/StoreThanatopractitionerRequest.php new file mode 100644 index 0000000..7ad6870 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreThanatopractitionerRequest.php @@ -0,0 +1,57 @@ + + */ + public function rules(): array + { + return [ + 'employee_id' => 'required|exists:employees,id|unique:thanatopractitioners,employee_id', + 'diploma_number' => 'nullable|string|max:191', + 'diploma_date' => 'nullable|date', + 'authorization_number' => 'nullable|string|max:191', + 'authorization_issue_date' => 'nullable|date', + 'authorization_expiry_date' => 'nullable|date|after_or_equal:authorization_issue_date', + 'notes' => 'nullable|string', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'employee_id.required' => 'L\'employé est obligatoire.', + 'employee_id.exists' => 'L\'employé sélectionné n\'existe pas.', + 'employee_id.unique' => 'Cet employé est déjà enregistré comme thanatopractitioner.', + 'diploma_number.string' => 'Le numéro de diplôme doit être une chaîne de caractères.', + 'diploma_number.max' => 'Le numéro de diplôme ne peut pas dépasser :max caractères.', + 'diploma_date.date' => 'La date d\'obtention du diplôme doit être une date valide.', + 'authorization_number.string' => 'Le numéro d\'autorisation doit être une chaîne de caractères.', + 'authorization_number.max' => 'Le numéro d\'autorisation ne peut pas dépasser :max caractères.', + 'authorization_issue_date.date' => 'La date de délivrance de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.date' => 'La date d\'expiration de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'notes.string' => 'Les notes doivent être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php b/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php new file mode 100644 index 0000000..9f9784e --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateEmployeeRequest.php @@ -0,0 +1,65 @@ + + */ + public function rules(): array + { + return [ + 'first_name' => 'required|string|max:191', + 'last_name' => 'required|string|max:191', + 'email' => [ + 'nullable', + 'email', + 'max:191', + Rule::unique('employees', 'email')->ignore($this->route('employee')) + ], + 'phone' => 'nullable|string|max:50', + 'job_title' => 'nullable|string|max:191', + 'hire_date' => 'nullable|date', + 'active' => 'boolean', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'first_name.required' => 'Le prénom est obligatoire.', + 'first_name.string' => 'Le prénom doit être une chaîne de caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser :max caractères.', + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.string' => 'Le nom de famille doit être une chaîne de caractères.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser :max caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.unique' => 'Cette adresse email est déjà utilisée.', + 'phone.string' => 'Le téléphone doit être une chaîne de caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser :max caractères.', + 'job_title.string' => 'L\'intitulé du poste doit être une chaîne de caractères.', + 'job_title.max' => 'L\'intitulé du poste ne peut pas dépasser :max caractères.', + 'hire_date.date' => 'La date d\'embauche doit être une date valide.', + 'active.boolean' => 'Le statut actif doit être un booléen.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdatePractitionerDocumentRequest.php b/thanasoft-back/app/Http/Requests/UpdatePractitionerDocumentRequest.php new file mode 100644 index 0000000..6ee2815 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdatePractitionerDocumentRequest.php @@ -0,0 +1,55 @@ + + */ + public function rules(): array + { + return [ + 'practitioner_id' => 'required|exists:thanatopractitioners,id', + 'doc_type' => 'required|string|max:191', + 'file_id' => 'nullable|exists:files,id', + 'issue_date' => 'nullable|date', + 'expiry_date' => 'nullable|date|after_or_equal:issue_date', + 'status' => 'nullable|string|max:64', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'practitioner_id.required' => 'Le thanatopractitioner est obligatoire.', + 'practitioner_id.exists' => 'Le thanatopractitioner sélectionné n\'existe pas.', + 'doc_type.required' => 'Le type de document est obligatoire.', + 'doc_type.string' => 'Le type de document doit être une chaîne de caractères.', + 'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.', + 'file_id.exists' => 'Le fichier sélectionné n\'existe pas.', + 'issue_date.date' => 'La date de délivrance doit être une date valide.', + 'expiry_date.date' => 'La date d\'expiration doit être une date valide.', + 'expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'status.string' => 'Le statut doit être une chaîne de caractères.', + 'status.max' => 'Le statut ne peut pas dépasser :max caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateProductRequest.php b/thanasoft-back/app/Http/Requests/UpdateProductRequest.php index ef92b38..14b27b0 100644 --- a/thanasoft-back/app/Http/Requests/UpdateProductRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateProductRequest.php @@ -26,7 +26,7 @@ class UpdateProductRequest extends FormRequest return [ 'nom' => 'required|string|max:255', 'reference' => "nullable", - 'categorie' => 'required|string|max:191', + 'categorie_id' => 'required|exists:product_categories,id', 'fabricant' => 'nullable|string|max:191', 'stock_actuel' => 'required|numeric|min:0', 'stock_minimum' => 'required|numeric|min:0', @@ -54,9 +54,8 @@ class UpdateProductRequest extends FormRequest 'reference.string' => 'La référence du produit doit être une chaîne de caractères.', 'reference.max' => 'La référence du produit ne peut pas dépasser 100 caractères.', 'reference.unique' => 'Cette référence de produit existe déjà.', - 'categorie.required' => 'La catégorie est obligatoire.', - 'categorie.string' => 'La catégorie doit être une chaîne de caractères.', - 'categorie.max' => 'La catégorie ne peut pas dépasser 191 caractères.', + 'categorie_id.required' => 'La catégorie est obligatoire.', + 'categorie_id.exists' => 'La catégorie sélectionnée n\'existe pas.', 'fabricant.string' => 'Le fabricant doit être une chaîne de caractères.', 'fabricant.max' => 'Le fabricant ne peut pas dépasser 191 caractères.', 'stock_actuel.required' => 'Le stock actuel est obligatoire.', diff --git a/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php b/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php new file mode 100644 index 0000000..bb9aff4 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php @@ -0,0 +1,62 @@ + + */ + public function rules(): array + { + return [ + 'employee_id' => [ + 'required', + 'exists:employees,id', + Rule::unique('thanatopractitioners', 'employee_id')->ignore($this->route('thanatopractitioner')) + ], + 'diploma_number' => 'nullable|string|max:191', + 'diploma_date' => 'nullable|date', + 'authorization_number' => 'nullable|string|max:191', + 'authorization_issue_date' => 'nullable|date', + 'authorization_expiry_date' => 'nullable|date|after_or_equal:authorization_issue_date', + 'notes' => 'nullable|string', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'employee_id.required' => 'L\'employé est obligatoire.', + 'employee_id.exists' => 'L\'employé sélectionné n\'existe pas.', + 'employee_id.unique' => 'Cet employé est déjà enregistré comme thanatopractitioner.', + 'diploma_number.string' => 'Le numéro de diplôme doit être une chaîne de caractères.', + 'diploma_number.max' => 'Le numéro de diplôme ne peut pas dépasser :max caractères.', + 'diploma_date.date' => 'La date d\'obtention du diplôme doit être une date valide.', + 'authorization_number.string' => 'Le numéro d\'autorisation doit être une chaîne de caractères.', + 'authorization_number.max' => 'Le numéro d\'autorisation ne peut pas dépasser :max caractères.', + 'authorization_issue_date.date' => 'La date de délivrance de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.date' => 'La date d\'expiration de l\'autorisation doit être une date valide.', + 'authorization_expiry_date.after_or_equal' => 'La date d\'expiration doit être égale ou postérieure à la date de délivrance.', + 'notes.string' => 'Les notes doivent être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/EmployeeCollection.php b/thanasoft-back/app/Http/Resources/Employee/EmployeeCollection.php new file mode 100644 index 0000000..0c54e46 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/EmployeeCollection.php @@ -0,0 +1,43 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($employee) { + return [ + 'id' => $employee->id, + 'first_name' => $employee->first_name, + 'last_name' => $employee->last_name, + 'full_name' => $employee->full_name, + 'email' => $employee->email, + 'phone' => $employee->phone, + 'job_title' => $employee->job_title, + 'hire_date' => $employee->hire_date?->format('Y-m-d'), + 'active' => $employee->active, + 'created_at' => $employee->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $employee->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $employee->thanatopractitioner ? [ + 'id' => $employee->thanatopractitioner->id, + 'diploma_number' => $employee->thanatopractitioner->diploma_number, + 'authorization_number' => $employee->thanatopractitioner->authorization_number, + 'is_authorization_valid' => $employee->thanatopractitioner->is_authorization_valid, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php b/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php new file mode 100644 index 0000000..84dc821 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/EmployeeResource.php @@ -0,0 +1,37 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'full_name' => $this->full_name, + 'email' => $this->email, + 'phone' => $this->phone, + 'job_title' => $this->job_title, + 'hire_date' => $this->hire_date?->format('Y-m-d'), + 'active' => $this->active, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $this->when( + $this->relationLoaded('thanatopractitioner'), + new ThanatopractitionerResource($this->thanatopractitioner) + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentCollection.php b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentCollection.php new file mode 100644 index 0000000..9cc2780 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentCollection.php @@ -0,0 +1,48 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($document) { + return [ + 'id' => $document->id, + 'practitioner_id' => $document->practitioner_id, + 'doc_type' => $document->doc_type, + 'file_id' => $document->file_id, + 'issue_date' => $document->issue_date?->format('Y-m-d'), + 'expiry_date' => $document->expiry_date?->format('Y-m-d'), + 'status' => $document->status, + 'is_valid' => $document->is_valid, + 'created_at' => $document->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $document->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $document->thanatopractitioner ? [ + 'id' => $document->thanatopractitioner->id, + 'employee_id' => $document->thanatopractitioner->employee_id, + 'diploma_number' => $document->thanatopractitioner->diploma_number, + 'authorization_number' => $document->thanatopractitioner->authorization_number, + 'employee' => $document->thanatopractitioner->employee ? [ + 'id' => $document->thanatopractitioner->employee->id, + 'first_name' => $document->thanatopractitioner->employee->first_name, + 'last_name' => $document->thanatopractitioner->employee->last_name, + 'full_name' => $document->thanatopractitioner->employee->full_name, + ] : null, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentResource.php b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentResource.php new file mode 100644 index 0000000..dd467d2 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/PractitionerDocumentResource.php @@ -0,0 +1,36 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'practitioner_id' => $this->practitioner_id, + 'doc_type' => $this->doc_type, + 'file_id' => $this->file_id, + 'issue_date' => $this->issue_date?->format('Y-m-d'), + 'expiry_date' => $this->expiry_date?->format('Y-m-d'), + 'status' => $this->status, + 'is_valid' => $this->is_valid, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'thanatopractitioner' => $this->when( + $this->relationLoaded('thanatopractitioner'), + new ThanatopractitionerResource($this->thanatopractitioner) + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerCollection.php b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerCollection.php new file mode 100644 index 0000000..d86aad5 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerCollection.php @@ -0,0 +1,45 @@ + + */ + public function toArray($request): array + { + return [ + 'data' => $this->collection->map(function ($thanatopractitioner) { + return [ + 'id' => $thanatopractitioner->id, + 'employee_id' => $thanatopractitioner->employee_id, + 'diploma_number' => $thanatopractitioner->diploma_number, + 'diploma_date' => $thanatopractitioner->diploma_date?->format('Y-m-d'), + 'authorization_number' => $thanatopractitioner->authorization_number, + 'authorization_issue_date' => $thanatopractitioner->authorization_issue_date?->format('Y-m-d'), + 'authorization_expiry_date' => $thanatopractitioner->authorization_expiry_date?->format('Y-m-d'), + 'notes' => $thanatopractitioner->notes, + 'is_authorization_valid' => $thanatopractitioner->is_authorization_valid, + 'created_at' => $thanatopractitioner->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $thanatopractitioner->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'employee' => $thanatopractitioner->employee ? [ + 'id' => $thanatopractitioner->employee->id, + 'first_name' => $thanatopractitioner->employee->first_name, + 'last_name' => $thanatopractitioner->employee->last_name, + 'full_name' => $thanatopractitioner->employee->full_name, + 'email' => $thanatopractitioner->employee->email, + 'job_title' => $thanatopractitioner->employee->job_title, + ] : null, + ]; + }), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerResource.php b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerResource.php new file mode 100644 index 0000000..97c787d --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Employee/ThanatopractitionerResource.php @@ -0,0 +1,41 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'employee_id' => $this->employee_id, + 'diploma_number' => $this->diploma_number, + 'diploma_date' => $this->diploma_date?->format('Y-m-d'), + 'authorization_number' => $this->authorization_number, + 'authorization_issue_date' => $this->authorization_issue_date?->format('Y-m-d'), + 'authorization_expiry_date' => $this->authorization_expiry_date?->format('Y-m-d'), + 'notes' => $this->notes, + 'is_authorization_valid' => $this->is_authorization_valid, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Relations + 'employee' => $this->when( + $this->relationLoaded('employee'), + new EmployeeResource($this->employee) + ), + 'documents' => $this->when( + $this->relationLoaded('documents'), + PractitionerDocumentResource::collection($this->documents) + ), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Product/ProductResource.php b/thanasoft-back/app/Http/Resources/Product/ProductResource.php index 6c7190a..30b0dcd 100644 --- a/thanasoft-back/app/Http/Resources/Product/ProductResource.php +++ b/thanasoft-back/app/Http/Resources/Product/ProductResource.php @@ -18,7 +18,7 @@ class ProductResource extends JsonResource 'id' => $this->id, 'nom' => $this->nom, 'reference' => $this->reference, - 'categorie' => $this->categorie, + 'categorie_id' => $this->categorie_id, 'fabricant' => $this->fabricant, 'stock_actuel' => $this->stock_actuel, 'stock_minimum' => $this->stock_minimum, @@ -47,6 +47,13 @@ class ProductResource extends JsonResource 'email' => $this->fournisseur->email, ] : null; }), + 'category' => $this->whenLoaded('category', function() { + return $this->category ? [ + 'id' => $this->category->id, + 'name' => $this->category->name, + 'code' => $this->category->code, + ] : null; + }), ]; } diff --git a/thanasoft-back/app/Models/Employee.php b/thanasoft-back/app/Models/Employee.php new file mode 100644 index 0000000..9d0fe56 --- /dev/null +++ b/thanasoft-back/app/Models/Employee.php @@ -0,0 +1,85 @@ + + */ + protected $fillable = [ + 'first_name', + 'last_name', + 'email', + 'phone', + 'job_title', + 'hire_date', + 'active', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'active' => 'boolean', + 'hire_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the thanatopractitioner associated with the employee. + */ + public function thanatopractitioner(): HasOne + { + return $this->hasOne(Thanatopractitioner::class); + } + + /** + * Get the full name of the employee. + */ + public function getFullNameAttribute(): string + { + return $this->first_name . ' ' . $this->last_name; + } + + /** + * Scope a query to only include active employees. + */ + public function scopeActive($query) + { + return $query->where('active', true); + } + + /** + * Scope a query to only include inactive employees. + */ + public function scopeInactive($query) + { + return $query->where('active', false); + } + + /** + * Scope a query to search employees. + */ + public function scopeSearch($query, string $term) + { + return $query->where(function ($q) use ($term) { + $q->where('first_name', 'like', '%' . $term . '%') + ->orWhere('last_name', 'like', '%' . $term . '%') + ->orWhere('email', 'like', '%' . $term . '%') + ->orWhere('job_title', 'like', '%' . $term . '%'); + }); + } +} diff --git a/thanasoft-back/app/Models/PractitionerDocument.php b/thanasoft-back/app/Models/PractitionerDocument.php new file mode 100644 index 0000000..66d1f9d --- /dev/null +++ b/thanasoft-back/app/Models/PractitionerDocument.php @@ -0,0 +1,86 @@ + + */ + protected $fillable = [ + 'practitioner_id', + 'doc_type', + 'file_id', + 'issue_date', + 'expiry_date', + 'status', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'issue_date' => 'date', + 'expiry_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the thanatopractitioner that owns the document. + */ + public function thanatopractitioner(): BelongsTo + { + return $this->belongsTo(Thanatopractitioner::class, 'practitioner_id'); + } + + /** + * Scope a query to only include documents with valid expiry date. + */ + public function scopeValid($query) + { + return $query->where(function ($q) { + $q->whereNull('expiry_date') + ->orWhere('expiry_date', '>=', now()); + }); + } + + /** + * Scope a query to only include documents with expired expiry date. + */ + public function scopeExpired($query) + { + return $query->whereNotNull('expiry_date') + ->where('expiry_date', '<', now()); + } + + /** + * Scope a query to filter by document type. + */ + public function scopeOfType($query, string $type) + { + return $query->where('doc_type', $type); + } + + /** + * Check if the document is still valid. + */ + public function getIsValidAttribute(): bool + { + if (!$this->expiry_date) { + return true; // No expiry date means it's valid + } + + return $this->expiry_date >= now(); + } +} diff --git a/thanasoft-back/app/Models/Product.php b/thanasoft-back/app/Models/Product.php index c35a147..fde2923 100644 --- a/thanasoft-back/app/Models/Product.php +++ b/thanasoft-back/app/Models/Product.php @@ -11,7 +11,7 @@ class Product extends Model protected $fillable = [ 'nom', 'reference', - 'categorie', + 'categorie_id', 'fabricant', 'stock_actuel', 'stock_minimum', @@ -48,7 +48,7 @@ class Product extends Model */ public function category(): BelongsTo { - return $this->belongsTo(ProductCategory::class, 'categorie', 'name'); + return $this->belongsTo(ProductCategory::class); } /** diff --git a/thanasoft-back/app/Models/ProductCategory.php b/thanasoft-back/app/Models/ProductCategory.php index 9caca89..87f68f0 100644 --- a/thanasoft-back/app/Models/ProductCategory.php +++ b/thanasoft-back/app/Models/ProductCategory.php @@ -41,7 +41,7 @@ class ProductCategory extends Model */ public function products(): HasMany { - return $this->hasMany(Product::class, 'categorie', 'name'); + return $this->hasMany(Product::class, 'categorie_id'); } /** diff --git a/thanasoft-back/app/Models/Thanatopractitioner.php b/thanasoft-back/app/Models/Thanatopractitioner.php new file mode 100644 index 0000000..df6ca07 --- /dev/null +++ b/thanasoft-back/app/Models/Thanatopractitioner.php @@ -0,0 +1,85 @@ + + */ + protected $fillable = [ + 'employee_id', + 'diploma_number', + 'diploma_date', + 'authorization_number', + 'authorization_issue_date', + 'authorization_expiry_date', + 'notes', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'diploma_date' => 'date', + 'authorization_issue_date' => 'date', + 'authorization_expiry_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Get the employee that owns the thanatopractitioner. + */ + public function employee(): BelongsTo + { + return $this->belongsTo(Employee::class); + } + + /** + * Get all documents associated with the thanatopractitioner. + */ + public function documents(): HasMany + { + return $this->hasMany(PractitionerDocument::class, 'practitioner_id'); + } + + /** + * Scope a query to only include practitioners with valid authorization. + */ + public function scopeWithValidAuthorization($query) + { + return $query->where('authorization_expiry_date', '>=', now()); + } + + /** + * Scope a query to only include practitioners with expired authorization. + */ + public function scopeWithExpiredAuthorization($query) + { + return $query->where('authorization_expiry_date', '<', now()); + } + + /** + * Check if the authorization is still valid. + */ + public function getIsAuthorizationValidAttribute(): bool + { + if (!$this->authorization_expiry_date) { + return false; + } + + return $this->authorization_expiry_date >= now(); + } +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index c1e9f5b..76de503 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -43,6 +43,19 @@ class AppServiceProvider extends ServiceProvider $this->app->bind(\App\Repositories\ProductCategoryRepositoryInterface::class, function ($app) { return new \App\Repositories\ProductCategoryRepository($app->make(\App\Models\ProductCategory::class)); }); + + // Employee management repository bindings + $this->app->bind(\App\Repositories\EmployeeRepositoryInterface::class, function ($app) { + return new \App\Repositories\EmployeeRepository($app->make(\App\Models\Employee::class)); + }); + + $this->app->bind(\App\Repositories\ThanatopractitionerRepositoryInterface::class, function ($app) { + return new \App\Repositories\ThanatopractitionerRepository($app->make(\App\Models\Thanatopractitioner::class)); + }); + + $this->app->bind(\App\Repositories\PractitionerDocumentRepositoryInterface::class, function ($app) { + return new \App\Repositories\PractitionerDocumentRepository($app->make(\App\Models\PractitionerDocument::class)); + }); } /** diff --git a/thanasoft-back/app/Repositories/EmployeeRepository.php b/thanasoft-back/app/Repositories/EmployeeRepository.php new file mode 100644 index 0000000..587153d --- /dev/null +++ b/thanasoft-back/app/Repositories/EmployeeRepository.php @@ -0,0 +1,127 @@ +model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->search($filters['search']); + } + + if (isset($filters['active'])) { + if ($filters['active']) { + $query->active(); + } else { + $query->inactive(); + } + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'last_name'; + $sortDirection = $filters['sort_direction'] ?? 'asc'; + $query->orderBy($sortField, $sortDirection); + + return $query->get(); + } + + /** + * Find an employee by ID. + */ + public function findById(int $id): ?Employee + { + return $this->model->newQuery()->find($id); + } + + /** + * Find an employee by email. + */ + public function findByEmail(string $email): ?Employee + { + return $this->model->newQuery()->where('email', $email)->first(); + } + + /** + * Get active employees only. + */ + public function getActive(): Collection + { + return $this->model->newQuery()->active()->get(); + } + + /** + * Get inactive employees only. + */ + public function getInactive(): Collection + { + return $this->model->newQuery()->inactive()->get(); + } + + /** + * Search employees by term. + */ + public function search(string $term): Collection + { + return $this->model->newQuery()->search($term)->get(); + } + + /** + * Get employees with pagination. + */ + public function getPaginated(int $perPage = 10): array + { + $paginator = $this->model->newQuery()->paginate($perPage); + + return [ + 'employees' => $paginator->getCollection(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ], + ]; + } + + /** + * Get employees with their thanatopractitioner data. + */ + public function getWithThanatopractitioner(): Collection + { + return $this->model->newQuery() + ->with('thanatopractitioner') + ->orderBy('last_name') + ->get(); + } + + /** + * Get employee statistics. + */ + public function getStatistics(): array + { + return [ + 'total' => $this->model->newQuery()->count(), + 'active' => $this->model->newQuery()->active()->count(), + 'inactive' => $this->model->newQuery()->inactive()->count(), + 'with_thanatopractitioner' => $this->model->newQuery()->has('thanatopractitioner')->count(), + ]; + } +} diff --git a/thanasoft-back/app/Repositories/EmployeeRepositoryInterface.php b/thanasoft-back/app/Repositories/EmployeeRepositoryInterface.php new file mode 100644 index 0000000..bca85d4 --- /dev/null +++ b/thanasoft-back/app/Repositories/EmployeeRepositoryInterface.php @@ -0,0 +1,82 @@ + $filters + * @return Collection + */ + public function getAll(array $filters = []): Collection; + + /** + * Find an employee by ID. + * + * @param int $id + * @return Employee|null + */ + public function findById(int $id): ?Employee; + + /** + * Find an employee by email. + * + * @param string $email + * @return Employee|null + */ + public function findByEmail(string $email): ?Employee; + + /** + * Get active employees only. + * + * @return Collection + */ + public function getActive(): Collection; + + /** + * Get inactive employees only. + * + * @return Collection + */ + public function getInactive(): Collection; + + /** + * Search employees by term. + * + * @param string $term + * @return Collection + */ + public function search(string $term): Collection; + + /** + * Get employees with pagination. + * + * @param int $perPage + * @return array{employees: Collection, pagination: array} + */ + public function getPaginated(int $perPage = 10): array; + + /** + * Get employees with their thanatopractitioner data. + * + * @return Collection + */ + public function getWithThanatopractitioner(): Collection; + + /** + * Get employee statistics. + * + * @return array + */ + public function getStatistics(): array; +} diff --git a/thanasoft-back/app/Repositories/PractitionerDocumentRepository.php b/thanasoft-back/app/Repositories/PractitionerDocumentRepository.php new file mode 100644 index 0000000..175fe8f --- /dev/null +++ b/thanasoft-back/app/Repositories/PractitionerDocumentRepository.php @@ -0,0 +1,149 @@ +model->newQuery()->with(['thanatopractitioner.employee']); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('doc_type', 'like', '%' . $filters['search'] . '%') + ->orWhere('status', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['practitioner_id'])) { + $query->where('practitioner_id', $filters['practitioner_id']); + } + + if (!empty($filters['doc_type'])) { + $query->ofType($filters['doc_type']); + } + + if (isset($filters['valid_only'])) { + if ($filters['valid_only']) { + $query->valid(); + } else { + $query->expired(); + } + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->get(); + } + + /** + * Find a practitioner document by ID. + */ + public function findById(int $id): ?PractitionerDocument + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->find($id); + } + + /** + * Get documents by practitioner ID. + */ + public function getByPractitionerId(int $practitionerId): Collection + { + return $this->model->newQuery() + ->where('practitioner_id', $practitionerId) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * Get documents by type. + */ + public function getByDocumentType(string $docType): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->ofType($docType) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * Get valid documents (not expired). + */ + public function getValid(): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->valid() + ->orderBy('expiry_date') + ->get(); + } + + /** + * Get expired documents. + */ + public function getExpired(): Collection + { + return $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->expired() + ->orderBy('expiry_date', 'desc') + ->get(); + } + + /** + * Get documents with pagination. + */ + public function getPaginated(int $perPage = 10): array + { + $paginator = $this->model->newQuery() + ->with(['thanatopractitioner.employee']) + ->paginate($perPage); + + return [ + 'documents' => $paginator->getCollection(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ], + ]; + } + + /** + * Get document statistics. + */ + public function getStatistics(): array + { + return [ + 'total' => $this->model->newQuery()->count(), + 'valid' => $this->model->newQuery()->valid()->count(), + 'expired' => $this->model->newQuery()->expired()->count(), + 'by_type' => $this->model->newQuery() + ->selectRaw('doc_type, count(*) as count') + ->groupBy('doc_type') + ->pluck('count', 'doc_type') + ->toArray(), + ]; + } +} diff --git a/thanasoft-back/app/Repositories/PractitionerDocumentRepositoryInterface.php b/thanasoft-back/app/Repositories/PractitionerDocumentRepositoryInterface.php new file mode 100644 index 0000000..77b3116 --- /dev/null +++ b/thanasoft-back/app/Repositories/PractitionerDocumentRepositoryInterface.php @@ -0,0 +1,75 @@ + $filters + * @return Collection + */ + public function getAll(array $filters = []): Collection; + + /** + * Find a practitioner document by ID. + * + * @param int $id + * @return PractitionerDocument|null + */ + public function findById(int $id): ?PractitionerDocument; + + /** + * Get documents by practitioner ID. + * + * @param int $practitionerId + * @return Collection + */ + public function getByPractitionerId(int $practitionerId): Collection; + + /** + * Get documents by type. + * + * @param string $docType + * @return Collection + */ + public function getByDocumentType(string $docType): Collection; + + /** + * Get valid documents (not expired). + * + * @return Collection + */ + public function getValid(): Collection; + + /** + * Get expired documents. + * + * @return Collection + */ + public function getExpired(): Collection; + + /** + * Get documents with pagination. + * + * @param int $perPage + * @return array{documents: Collection, pagination: array} + */ + public function getPaginated(int $perPage = 10): array; + + /** + * Get document statistics. + * + * @return array + */ + public function getStatistics(): array; +} diff --git a/thanasoft-back/app/Repositories/ProductRepository.php b/thanasoft-back/app/Repositories/ProductRepository.php index 03d536c..2196950 100644 --- a/thanasoft-back/app/Repositories/ProductRepository.php +++ b/thanasoft-back/app/Repositories/ProductRepository.php @@ -19,20 +19,19 @@ class ProductRepository extends BaseRepository implements ProductRepositoryInter */ public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator { - $query = $this->model->newQuery()->with('fournisseur'); + $query = $this->model->newQuery()->with(['fournisseur', 'category']); // Apply filters if (!empty($filters['search'])) { $query->where(function ($q) use ($filters) { $q->where('nom', 'like', '%' . $filters['search'] . '%') ->orWhere('reference', 'like', '%' . $filters['search'] . '%') - ->orWhere('categorie', 'like', '%' . $filters['search'] . '%') ->orWhere('fabricant', 'like', '%' . $filters['search'] . '%'); }); } if (!empty($filters['categorie'])) { - $query->where('categorie', $filters['categorie']); + $query->where('categorie_id', $filters['categorie']); } if (!empty($filters['fournisseur_id'])) { @@ -62,7 +61,7 @@ class ProductRepository extends BaseRepository implements ProductRepositoryInter public function getLowStockProducts(int $perPage = 15): LengthAwarePaginator { return $this->model->newQuery() - ->with('fournisseur') + ->with(['fournisseur', 'category']) ->whereRaw('stock_actuel <= stock_minimum') ->orderBy('stock_actuel', 'asc') ->paginate($perPage); @@ -73,7 +72,7 @@ class ProductRepository extends BaseRepository implements ProductRepositoryInter */ public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false) { - $query = $this->model->newQuery()->with('fournisseur'); + $query = $this->model->newQuery()->with(['fournisseur', 'category']); if ($exactMatch) { $query->where('nom', $name); @@ -87,11 +86,11 @@ class ProductRepository extends BaseRepository implements ProductRepositoryInter /** * Get products by category */ - public function getByCategory(string $category, int $perPage = 15): LengthAwarePaginator + public function getByCategory(int $categoryId, int $perPage = 15): LengthAwarePaginator { return $this->model->newQuery() - ->with('fournisseur') - ->where('categorie', $category) + ->with(['fournisseur', 'category']) + ->where('categorie_id', $categoryId) ->orderBy('nom') ->paginate($perPage); } diff --git a/thanasoft-back/app/Repositories/ProductRepository.phpmodel->newQuery()->with('fournisseur'); - - // Apply filters - if (!empty($filters['search'])) { - $query->where(function ($q) use ($filters) { - $q->where('nom', 'like', '%' . $filters['search'] . '%') - ->orWhere('reference', 'like', '%' . $filters['search'] . '%') - ->orWhere('categorie', 'like', '%' . $filters['search'] . '%') - ->orWhere('fabricant', 'like', '%' . $filters['search'] . '%'); - }); - } - - if (!empty($filters['categorie'])) { - $query->where('categorie', $filters['categorie']); - } - - if (!empty($filters['fournisseur_id'])) { - $query->where('fournisseur_id', $filters['fournisseur_id']); - } - - if (isset($filters['low_stock'])) { - $query->whereRaw('stock_actuel <= stock_minimum'); - } - - if (isset($filters['expiring_soon'])) { - $query->where('date_expiration', '<=', now()->addDays(30)->toDateString()) - ->where('date_expiration', '>=', now()->toDateString()); - } - - // Apply sorting - $sortField = $filters['sort_by'] ?? 'created_at'; - $sortDirection = $filters['sort_direction'] ?? 'desc'; - $query->orderBy($sortField, $sortDirection); - - return $query->paginate($perPage); - } - - /** - * Get products with low stock - */ - public function getLowStockProducts(int $perPage = 15): LengthAwarePaginator - { - return $this->model->newQuery() - ->with('fournisseur') - ->whereRaw('stock_actuel <= stock_minimum') - ->orderBy('stock_actuel', 'asc') - ->paginate($perPage); - } - - /** - * Search products by name - */ - public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false) - { - $query = $this->model->newQuery()->with('fournisseur'); - - if ($exactMatch) { - $query->where('nom', $name); - } else { - $query->where('nom', 'like', '%' . $name . '%'); - } - - return $query->paginate($perPage); - } - - /** - * Get products by category - */ - public function getByCategory(string $category, int $perPage = 15): LengthAwarePaginator - { - return $this->model->newQuery() - ->with('fournisseur') - ->where('categorie', $category) - ->orderBy('nom') - ->paginate($perPage); - } - - /** - * Get products by fournisseur - */ - public function getProductsByFournisseur(int $fournisseurId): LengthAwarePaginator - { - return $this->model->newQuery() - ->where('fournisseur_id', $fournisseurId) - ->orderBy('nom') - ->paginate(15); - } - - /** - * Update stock quantity - */ - public function updateStock(int $productId, float $newQuantity): bool - { - return $this->model->where('id', $productId) - ->update(['stock_actuel' => $newQuantity]) > 0; - } - - /** - * Get product statistics - */ - public function getStatistics(): array - { - $totalProducts = $this->model->count(); - $lowStockProducts = $this->model->whereRaw('stock_actuel <= stock_minimum')->count(); - $expiringProducts = $this->model->where('date_expiration', '<=', now()->addDays(30)->toDateString()) - ->where('date_expiration', '>=', now()->toDateString()) - ->count(); - $totalValue = $this->model->sum(\DB::raw('stock_actuel * prix_unitaire')); - - return [ - 'total_products' => $totalProducts, - 'low_stock_products' => $lowStockProducts, - 'expiring_products' => $expiringProducts, - 'total_value' => $totalValue, - ]; - } -} diff --git a/thanasoft-back/app/Repositories/ThanatopractitionerRepository.php b/thanasoft-back/app/Repositories/ThanatopractitionerRepository.php new file mode 100644 index 0000000..f199753 --- /dev/null +++ b/thanasoft-back/app/Repositories/ThanatopractitionerRepository.php @@ -0,0 +1,135 @@ +model->newQuery()->with(['employee']); + + // Apply filters + if (!empty($filters['search'])) { + $query->whereHas('employee', function ($q) use ($filters) { + $q->search($filters['search']); + }); + } + + if (isset($filters['valid_authorization'])) { + if ($filters['valid_authorization']) { + $query->withValidAuthorization(); + } else { + $query->withExpiredAuthorization(); + } + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->get(); + } + + /** + * Find a thanatopractitioner by ID. + */ + public function findById(int $id): ?Thanatopractitioner + { + return $this->model->newQuery() + ->with(['employee', 'documents']) + ->find($id); + } + + /** + * Find a thanatopractitioner by employee ID. + */ + public function findByEmployeeId(int $employeeId): ?Thanatopractitioner + { + return $this->model->newQuery() + ->with(['employee', 'documents']) + ->where('employee_id', $employeeId) + ->first(); + } + + /** + * Get thanatopractitioners with valid authorization. + */ + public function getWithValidAuthorization(): Collection + { + return $this->model->newQuery() + ->with(['employee']) + ->withValidAuthorization() + ->orderBy('authorization_expiry_date') + ->get(); + } + + /** + * Get thanatopractitioners with expired authorization. + */ + public function getWithExpiredAuthorization(): Collection + { + return $this->model->newQuery() + ->with(['employee']) + ->withExpiredAuthorization() + ->orderBy('authorization_expiry_date', 'desc') + ->get(); + } + + /** + * Get thanatopractitioners with their complete data. + */ + public function getWithRelations(): Collection + { + return $this->model->newQuery() + ->with(['employee', 'documents']) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * Get thanatopractitioners with pagination. + */ + public function getPaginated(int $perPage = 10): array + { + $paginator = $this->model->newQuery() + ->with(['employee']) + ->paginate($perPage); + + return [ + 'thanatopractitioners' => $paginator->getCollection(), + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ], + ]; + } + + /** + * Get thanatopractitioner statistics. + */ + public function getStatistics(): array + { + return [ + 'total' => $this->model->newQuery()->count(), + 'with_valid_authorization' => $this->model->newQuery()->withValidAuthorization()->count(), + 'with_expired_authorization' => $this->model->newQuery()->withExpiredAuthorization()->count(), + 'with_documents' => $this->model->newQuery()->has('documents')->count(), + ]; + } +} diff --git a/thanasoft-back/app/Repositories/ThanatopractitionerRepositoryInterface.php b/thanasoft-back/app/Repositories/ThanatopractitionerRepositoryInterface.php new file mode 100644 index 0000000..4e12187 --- /dev/null +++ b/thanasoft-back/app/Repositories/ThanatopractitionerRepositoryInterface.php @@ -0,0 +1,74 @@ + $filters + * @return Collection + */ + public function getAll(array $filters = []): Collection; + + /** + * Find a thanatopractitioner by ID. + * + * @param int $id + * @return Thanatopractitioner|null + */ + public function findById(int $id): ?Thanatopractitioner; + + /** + * Find a thanatopractitioner by employee ID. + * + * @param int $employeeId + * @return Thanatopractitioner|null + */ + public function findByEmployeeId(int $employeeId): ?Thanatopractitioner; + + /** + * Get thanatopractitioners with valid authorization. + * + * @return Collection + */ + public function getWithValidAuthorization(): Collection; + + /** + * Get thanatopractitioners with expired authorization. + * + * @return Collection + */ + public function getWithExpiredAuthorization(): Collection; + + /** + * Get thanatopractitioners with their complete data. + * + * @return Collection + */ + public function getWithRelations(): Collection; + + /** + * Get thanatopractitioners with pagination. + * + * @param int $perPage + * @return array{thanatopractitioners: Collection, pagination: array} + */ + public function getPaginated(int $perPage = 10): array; + + /** + * Get thanatopractitioner statistics. + * + * @return array + */ + public function getStatistics(): array; +} diff --git a/thanasoft-back/database/migrations/2025_11_05_070647_change_categorie_to_categorie_id_in_products_table.php b/thanasoft-back/database/migrations/2025_11_05_070647_change_categorie_to_categorie_id_in_products_table.php new file mode 100644 index 0000000..944b2cd --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_070647_change_categorie_to_categorie_id_in_products_table.php @@ -0,0 +1,60 @@ +foreignId('categorie_id')->nullable()->after('reference') + ->constrained('product_categories')->onDelete('set null'); + }); + + // Migrate existing data: map categorie string values to ProductCategory IDs + $categories = ProductCategory::all()->keyBy('name'); + + Product::chunk(100, function ($products) use ($categories) { + foreach ($products as $product) { + if ($product->categorie && isset($categories[$product->categorie])) { + $product->categorie_id = $categories[$product->categorie]->id; + $product->save(); + } + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // First, we need to restore the categorie data from categorie_id + $categories = ProductCategory::all()->keyBy('id'); + + Product::chunk(100, function ($products) use ($categories) { + foreach ($products as $product) { + if ($product->categorie_id && isset($categories[$product->categorie_id])) { + $product->categorie = $categories[$product->categorie_id]->name; + $product->save(); + } + } + }); + + Schema::table('products', function (Blueprint $table) { + // Drop the foreign key constraint first + $table->dropForeign(['categorie_id']); + + // Drop the categorie_id column + $table->dropColumn('categorie_id'); + }); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_115756_create_employees_table.php b/thanasoft-back/database/migrations/2025_11_05_115756_create_employees_table.php new file mode 100644 index 0000000..121de6f --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_115756_create_employees_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('first_name', 191)->comment('Prénom de l\'employé'); + $table->string('last_name', 191)->comment('Nom de famille de l\'employé'); + $table->string('email', 191)->nullable()->comment('Adresse email de l\'employé'); + $table->string('phone', 50)->nullable()->comment('Numéro de téléphone de l\'employé'); + $table->string('job_title', 191)->nullable()->comment('Intitulé du poste'); + $table->date('hire_date')->nullable()->comment('Date d\'embauche'); + $table->boolean('active')->default(true)->comment('Statut actif de l\'employé'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employees'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_115807_create_thanatopractitioners_table.php b/thanasoft-back/database/migrations/2025_11_05_115807_create_thanatopractitioners_table.php new file mode 100644 index 0000000..6b56747 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_115807_create_thanatopractitioners_table.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedBigInteger('employee_id')->unique()->comment('ID de l\'employé associé'); + $table->string('diploma_number', 191)->nullable()->comment('Numéro de diplôme'); + $table->date('diploma_date')->nullable()->comment('Date d\'obtention du diplôme'); + $table->string('authorization_number', 191)->nullable()->comment('Numéro d\'autorisation'); + $table->date('authorization_issue_date')->nullable()->comment('Date de délivrance de l\'autorisation'); + $table->date('authorization_expiry_date')->nullable()->comment('Date d\'expiration de l\'autorisation'); + $table->text('notes')->nullable()->comment('Notes supplémentaires'); + $table->timestamps(); + + $table->foreign('employee_id') + ->references('id') + ->on('employees') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('thanatopractitioners'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_05_115820_create_practitioner_documents_table.php b/thanasoft-back/database/migrations/2025_11_05_115820_create_practitioner_documents_table.php new file mode 100644 index 0000000..85f7ff9 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_05_115820_create_practitioner_documents_table.php @@ -0,0 +1,44 @@ +id(); + $table->unsignedBigInteger('practitioner_id')->comment('ID du thanatopractitioner'); + $table->string('doc_type', 191)->comment('Type de document'); + $table->unsignedBigInteger('file_id')->nullable()->comment('ID du fichier associé'); + $table->date('issue_date')->nullable()->comment('Date de délivrance'); + $table->date('expiry_date')->nullable()->comment('Date d\'expiration'); + $table->string('status', 64)->nullable()->comment('Statut du document'); + $table->timestamps(); + + $table->foreign('practitioner_id') + ->references('id') + ->on('thanatopractitioners') + ->onDelete('cascade'); + + // Note: The files table might not exist yet, so we won't add this foreign key constraint + // $table->foreign('file_id') + // ->references('id') + // ->on('files') + // ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('practitioner_documents'); + } +}; diff --git a/thanasoft-back/database/seeders/DatabaseSeeder.php b/thanasoft-back/database/seeders/DatabaseSeeder.php index 50dd255..fe09534 100644 --- a/thanasoft-back/database/seeders/DatabaseSeeder.php +++ b/thanasoft-back/database/seeders/DatabaseSeeder.php @@ -21,5 +21,7 @@ class DatabaseSeeder extends Seeder ]); $this->call(ProductCategorySeeder::class); + $this->call(EmployeeSeeder::class); + $this->call(ThanatopractitionerSeeder::class); } } diff --git a/thanasoft-back/database/seeders/EmployeeSeeder.php b/thanasoft-back/database/seeders/EmployeeSeeder.php new file mode 100644 index 0000000..812d3f8 --- /dev/null +++ b/thanasoft-back/database/seeders/EmployeeSeeder.php @@ -0,0 +1,134 @@ + 'Jean', + 'last_name' => 'Dupont', + 'email' => 'jean.dupont@thanasoft.com', + 'phone' => '+261341234567', + 'job_title' => 'Développeur Full-Stack', + 'hire_date' => '2022-01-15', + 'active' => true, + ], + [ + 'first_name' => 'Marie', + 'last_name' => 'Rasoa', + 'email' => 'marie.rasoa@thanasoft.com', + 'phone' => '+261341234569', + 'job_title' => 'Chef de Projet', + 'hire_date' => '2021-08-01', + 'active' => true, + ], + [ + 'first_name' => 'Paul', + 'last_name' => 'Ramanana', + 'email' => 'paul.ramanana@thanasoft.com', + 'phone' => '+261341234571', + 'job_title' => 'Designer UX/UI', + 'hire_date' => '2022-03-10', + 'active' => true, + ], + [ + 'first_name' => 'Sophie', + 'last_name' => 'Andriamatoa', + 'email' => 'sophie.andriamatoa@thanasoft.com', + 'phone' => '+261341234573', + 'job_title' => 'Responsable RH', + 'hire_date' => '2020-09-15', + 'active' => true, + ], + [ + 'first_name' => 'David', + 'last_name' => 'Randria', + 'email' => 'david.randria@thanasoft.com', + 'phone' => '+261341234575', + 'job_title' => 'Développeur Backend', + 'hire_date' => '2023-01-20', + 'active' => true, + ], + [ + 'first_name' => 'Lina', + 'last_name' => 'Ramaniraka', + 'email' => 'lina.ramaniraka@thanasoft.com', + 'phone' => '+261341234577', + 'job_title' => 'Comptable', + 'hire_date' => '2021-06-01', + 'active' => true, + ], + [ + 'first_name' => 'Marc', + 'last_name' => 'Andriantsoa', + 'email' => 'marc.andriantsoa@thanasoft.com', + 'phone' => '+261341234579', + 'job_title' => 'DevOps Engineer', + 'hire_date' => '2022-11-05', + 'active' => true, + ], + [ + 'first_name' => 'Julie', + 'last_name' => 'Rakotomalala', + 'email' => 'julie.rakotomalala@thanasoft.com', + 'phone' => '+261341234581', + 'job_title' => 'Community Manager', + 'hire_date' => '2023-03-15', + 'active' => true, + ], + [ + 'first_name' => 'Philippe', + 'last_name' => 'Rakoto', + 'email' => 'philippe.rakoto@thanasoft.com', + 'phone' => '+261341234583', + 'job_title' => 'Développeur Mobile', + 'hire_date' => '2023-06-10', + 'active' => true, + ], + [ + 'first_name' => 'Anne', + 'last_name' => 'Andriamanjato', + 'email' => 'anne.andriamanjato@thanasoft.com', + 'phone' => '+261341234585', + 'job_title' => 'Stagiaire', + 'hire_date' => '2024-01-15', + 'active' => true, + ], + ]; + + foreach ($employees as $employeeData) { + Employee::create($employeeData); + } + + // Create some inactive employees for testing + Employee::create([ + 'first_name' => 'Ancien', + 'last_name' => 'Employe', + 'email' => 'ancien.employe@thanasoft.com', + 'phone' => '+261341234587', + 'job_title' => 'Testeur', + 'hire_date' => '2020-01-01', + 'active' => false, + ]); + + Employee::create([ + 'first_name' => 'Employe', + 'last_name' => 'Suspendu', + 'email' => 'employe.suspendu@thanasoft.com', + 'phone' => '+261341234589', + 'job_title' => 'Assistant', + 'hire_date' => '2021-05-01', + 'active' => false, + ]); + } +} diff --git a/thanasoft-back/database/seeders/ThanatopractitionerSeeder.php b/thanasoft-back/database/seeders/ThanatopractitionerSeeder.php new file mode 100644 index 0000000..2b7b361 --- /dev/null +++ b/thanasoft-back/database/seeders/ThanatopractitionerSeeder.php @@ -0,0 +1,277 @@ + 'Jean-Baptiste', + 'last_name' => 'Ramanana', + 'email' => 'jb.ramanana@thanasoft.com', + 'phone' => '+261341235001', + 'job_title' => 'Thanatopracteur Senior', + 'hire_date' => '2020-08-01', + 'active' => true, + ], + [ + 'first_name' => 'Marie', + 'last_name' => 'Andriamanantsoa', + 'email' => 'marie.andriamanantsoa@thanasoft.com', + 'phone' => '+261341235003', + 'job_title' => 'Thanatopracteur Spécialisé', + 'hire_date' => '2019-07-01', + 'active' => true, + ], + [ + 'first_name' => 'Paul', + 'last_name' => 'Rakotomanga', + 'email' => 'paul.rakotomanga@thanasoft.com', + 'phone' => '+261341235005', + 'job_title' => 'Thanatopracteur Junior', + 'hire_date' => '2021-11-01', + 'active' => true, + ], + [ + 'first_name' => 'Sophie', + 'last_name' => 'Andriatiana', + 'email' => 'sophie.andriatiana@thanasoft.com', + 'phone' => '+261341235007', + 'job_title' => 'Thanatopracteur Expert', + 'hire_date' => '2018-06-01', + 'active' => true, + ], + [ + 'first_name' => 'David', + 'last_name' => 'Randriamahandry', + 'email' => 'david.randriamahandry@thanasoft.com', + 'phone' => '+261341235009', + 'job_title' => 'Thanatopracteur', + 'hire_date' => '2022-03-01', + 'active' => true, + ], + [ + 'first_name' => 'Lina', + 'last_name' => 'Ramaniraka', + 'email' => 'lina.ramaniraka@thanasoft.com', + 'phone' => '+261341235011', + 'job_title' => 'Thanatopracteur', + 'hire_date' => '2023-10-01', + 'active' => true, + ], + [ + 'first_name' => 'Marc', + 'last_name' => 'Andriamatsiroa', + 'email' => 'marc.andriamatsiroa@thanasoft.com', + 'phone' => '+261341235013', + 'job_title' => 'Thanatopracteur Chief', + 'hire_date' => '2018-01-01', + 'active' => true, + ], + [ + 'first_name' => 'Julie', + 'last_name' => 'Rakotomalala', + 'email' => 'julie.rakotomalala@thanasoft.com', + 'phone' => '+261341235015', + 'job_title' => 'Thanatopracteur Assistant', + 'hire_date' => '2024-05-01', + 'active' => true, + ], + ]; + + // Create employees and get their IDs + $employeeIds = []; + foreach ($employees as $employeeData) { + $employee = \App\Models\Employee::create($employeeData); + $employeeIds[] = $employee->id; + } + + // Create thanatopractitioners linked to the employees + $thanatopractitioners = [ + [ + 'employee_id' => $employeeIds[0], + 'diploma_number' => 'TP-DIPL-001-2020', + 'diploma_date' => '2020-06-15', + 'authorization_number' => 'TP-AUTH-001-2020', + 'authorization_issue_date' => '2020-07-01', + 'authorization_expiry_date' => '2025-07-01', + 'notes' => 'Thanatopracteur senior spécialisé en thanatopraxie générale et reconstructive.', + ], + [ + 'employee_id' => $employeeIds[1], + 'diploma_number' => 'TP-DIPL-002-2019', + 'diploma_date' => '2019-05-10', + 'authorization_number' => 'TP-AUTH-002-2019', + 'authorization_issue_date' => '2019-06-01', + 'authorization_expiry_date' => '2024-06-01', + 'notes' => 'Spécialiste en thanatopraxie pédiatrique avec 5 ans d\'expérience.', + ], + [ + 'employee_id' => $employeeIds[2], + 'diploma_number' => 'TP-DIPL-003-2021', + 'diploma_date' => '2021-09-22', + 'authorization_number' => 'TP-AUTH-003-2021', + 'authorization_issue_date' => '2021-10-01', + 'authorization_expiry_date' => '2026-10-01', + 'notes' => 'Thanatopracteur junior spécialisé en thanatopraxie esthétique.', + ], + [ + 'employee_id' => $employeeIds[3], + 'diploma_number' => 'TP-DIPL-004-2018', + 'diploma_date' => '2018-04-05', + 'authorization_number' => 'TP-AUTH-004-2018', + 'authorization_issue_date' => '2018-05-01', + 'authorization_expiry_date' => '2023-05-01', + 'notes' => 'Expert en thanatopraxie reconstructive et histopathologie.', + ], + [ + 'employee_id' => $employeeIds[4], + 'diploma_number' => 'TP-DIPL-005-2022', + 'diploma_date' => '2022-01-18', + 'authorization_number' => 'TP-AUTH-005-2022', + 'authorization_issue_date' => '2022-02-01', + 'authorization_expiry_date' => '2027-02-01', + 'notes' => 'Spécialiste en thanatopraxie traditionnelle et culturelle.', + ], + [ + 'employee_id' => $employeeIds[5], + 'diploma_number' => 'TP-DIPL-006-2023', + 'diploma_date' => '2023-08-12', + 'authorization_number' => 'TP-AUTH-006-2023', + 'authorization_issue_date' => '2023-09-01', + 'authorization_expiry_date' => '2028-09-01', + 'notes' => 'Thanatopracteur nouvellement certifiée, spécialisée en techniques modernes.', + ], + [ + 'employee_id' => $employeeIds[6], + 'diploma_number' => 'TP-DIPL-007-2017', + 'diploma_date' => '2017-11-30', + 'authorization_number' => 'TP-AUTH-007-2017', + 'authorization_issue_date' => '2018-01-01', + 'authorization_expiry_date' => '2023-01-01', + 'notes' => 'Responsable principal des thanatopracteurs, expert en histopathologie.', + ], + [ + 'employee_id' => $employeeIds[7], + 'diploma_number' => 'TP-DIPL-008-2024', + 'diploma_date' => '2024-03-25', + 'authorization_number' => 'TP-AUTH-008-2024', + 'authorization_issue_date' => '2024-04-01', + 'authorization_expiry_date' => '2029-04-01', + 'notes' => 'Thanatopracteur assistante, en formation continue en thanatopraxie assistée.', + ], + ]; + + foreach ($thanatopractitioners as $thanatopractitionerData) { + $thanatopractitioner = Thanatopractitioner::create($thanatopractitionerData); + + // Create some practitioner documents for each thanatopractitioner + $this->createPractitionerDocuments($thanatopractitioner); + } + + // Create inactive thanatopractitioners for testing + $inactiveEmployee1 = \App\Models\Employee::create([ + 'first_name' => 'Ancien', + 'last_name' => 'Thanatopracteur', + 'email' => 'ancien.thanato@thanasoft.com', + 'phone' => '+261341235017', + 'job_title' => 'Ancien Thanatopracteur', + 'hire_date' => '2015-03-01', + 'active' => false, + ]); + + $inactive1 = Thanatopractitioner::create([ + 'employee_id' => $inactiveEmployee1->id, + 'diploma_number' => 'TP-DIPL-TEST-001', + 'diploma_date' => '2015-01-01', + 'authorization_number' => 'TP-AUTH-TEST-001', + 'authorization_issue_date' => '2015-02-01', + 'authorization_expiry_date' => '2020-02-01', + 'notes' => 'Thanatopracteur inactif, autorisation expirée.', + ]); + + $this->createPractitionerDocuments($inactive1); + + $inactiveEmployee2 = \App\Models\Employee::create([ + 'first_name' => 'Thanatopracteur', + 'last_name' => 'Suspendu', + 'email' => 'thanato.suspendu@thanasoft.com', + 'phone' => '+261341235019', + 'job_title' => 'Thanatopracteur Suspendu', + 'hire_date' => '2018-08-01', + 'active' => false, + ]); + + $inactive2 = Thanatopractitioner::create([ + 'employee_id' => $inactiveEmployee2->id, + 'diploma_number' => 'TP-DIPL-TEST-002', + 'diploma_date' => '2018-06-01', + 'authorization_number' => 'TP-AUTH-TEST-002', + 'authorization_issue_date' => '2018-07-01', + 'authorization_expiry_date' => '2023-07-01', + 'notes' => 'Thanatopracteur temporairement suspendu.', + ]); + + $this->createPractitionerDocuments($inactive2); + } + + /** + * Create practitioner documents for a thanatopractitioner. + */ + private function createPractitionerDocuments(Thanatopractitioner $thanatopractitioner): void + { + $documents = [ + [ + 'doc_type' => 'diploma', + 'issue_date' => $thanatopractitioner->diploma_date, + 'expiry_date' => date('Y-m-d', strtotime('+5 years', strtotime($thanatopractitioner->diploma_date))), + 'status' => 'active', + ], + [ + 'doc_type' => 'certification', + 'issue_date' => date('Y-01-01'), + 'expiry_date' => date('Y-12-31'), + 'status' => 'active', + ], + ]; + + // Add specialized documents based on notes content + if (strpos($thanatopractitioner->notes, 'reconstructive') !== false || + strpos($thanatopractitioner->notes, 'histopathologie') !== false) { + $documents[] = [ + 'doc_type' => 'specialization', + 'issue_date' => $thanatopractitioner->diploma_date, + 'expiry_date' => date('Y-m-d', strtotime('+3 years', strtotime($thanatopractitioner->diploma_date))), + 'status' => 'active', + ]; + } + + // Add expired documents for inactive thanatopractitioners + $employee = $thanatopractitioner->employee; + if ($employee->active === false) { + $documents[] = [ + 'doc_type' => 'expired_certificate', + 'issue_date' => '2020-01-01', + 'expiry_date' => '2023-01-01', + 'status' => 'expired', + ]; + } + + foreach ($documents as $documentData) { + PractitionerDocument::create(array_merge($documentData, [ + 'practitioner_id' => $thanatopractitioner->id, + 'file_id' => null, // Will be set when files table is implemented + ])); + } + } +} diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 18aab33..fee0164 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -10,6 +10,9 @@ use App\Http\Controllers\Api\ClientCategoryController; use App\Http\Controllers\Api\FournisseurController; use App\Http\Controllers\Api\ProductController; use App\Http\Controllers\Api\ProductCategoryController; +use App\Http\Controllers\Api\EmployeeController; +use App\Http\Controllers\Api\ThanatopractitionerController; +use App\Http\Controllers\Api\PractitionerDocumentController; /* |-------------------------------------------------------------------------- @@ -73,4 +76,20 @@ Route::middleware('auth:sanctum')->group(function () { Route::get('/product-categories/statistics', [ProductCategoryController::class, 'statistics']); Route::apiResource('product-categories', ProductCategoryController::class); Route::patch('/product-categories/{id}/toggle-active', [ProductCategoryController::class, 'toggleActive']); + + // Employee management + Route::get('/employees/searchBy', [EmployeeController::class, 'searchBy']); + Route::get('/employees/thanatopractitioners', [EmployeeController::class, 'getThanatopractitioners']); + Route::apiResource('employees', EmployeeController::class); + + // Thanatopractitioner management + Route::apiResource('thanatopractitioners', ThanatopractitionerController::class); + Route::get('employees/{employeeId}/thanatopractitioners', [ThanatopractitionerController::class, 'getByEmployee']); + Route::get('/thanatopractitioners/{id}/documents', [PractitionerDocumentController::class, 'getByThanatopractitioner']); + + // Practitioner Document management + Route::get('/practitioner-documents/searchBy', [PractitionerDocumentController::class, 'searchBy']); + Route::get('/practitioner-documents/expiring', [PractitionerDocumentController::class, 'getExpiringDocuments']); + Route::apiResource('practitioner-documents', PractitionerDocumentController::class); + Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']); }); diff --git a/thanasoft-back/routes/web.php.server b/thanasoft-back/routes/web.php.server deleted file mode 100644 index 856b450..0000000 --- a/thanasoft-back/routes/web.php.server +++ /dev/null @@ -1,7 +0,0 @@ -where('any', '.*');