add back:

This commit is contained in:
Nyavokevin 2025-11-05 17:09:12 +03:00
parent 0ea8f1866b
commit e55cc5253e
46 changed files with 4212 additions and 560 deletions

88
th
View File

@ -1,88 +0,0 @@
<template>
<div :class="['stock-badge', stockClass]">
<div class="stock-info">
<span class="stock-label">{{ label }}</span>
<span class="stock-value">{{ value }} {{ unit }}</span>
</div>
<div v-if="showThreshold && threshold" class="stock-threshold">
<small>Min: {{ threshold }} {{ unit }}</small>
</div>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
const props = defineProps({
label: {
type: String,
required: true
},
value: {
type: Number,
required: true
},
unit: {
type: String,
default: ''
},
threshold: {
type: Number,
default: null
},
showThreshold: {
type: Boolean,
default: false
}
});
const stockClass = computed(() => {
if (props.threshold && props.value <= props.threshold) {
return 'stock-low';
}
return 'stock-normal';
});
</script>
<style scoped>
.stock-badge {
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid #e5e7eb;
background-color: #f9fafb;
}
.stock-low {
background-color: #fef2f2;
border-color: #fecaca;
color: #dc2626;
}
.stock-normal {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #166534;
}
.stock-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.stock-label {
font-weight: 500;
font-size: 0.875rem;
}
.stock-value {
font-weight: 700;
font-size: 1rem;
}
.stock-threshold {
font-size: 0.75rem;
opacity: 0.8;
}
</style>

611
thanas
View File

@ -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<Employee[]>([]);
const currentEmployee = ref<Employee | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const searchResults = ref<Employee[]>([]);
// Pagination state
const pagination = ref({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
from: 0,
to: 0,
});
// Getters
const allEmployees = computed(() => employees.value);
const activeEmployees = computed(() =>
employees.value.filter((employee) => employee.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
);
},
},
});
// Actions
fetchEmployees,
fetchEmployee,
createEmployee,
updateEmployee,
deleteEmployee,
searchEmployees,
toggleEmployeeStatus,
fetchStatistics,
clearCurrentEmployee,
clearStore,
clearError,
};
});

179
thanasoft
View File

@ -1,64 +1,141 @@
<template>
<div class="price-display">
<span class="price-amount">{{ formattedPrice }}</span>
<span v-if="currency" class="price-currency">{{ currency }}</span>
<span v-if="unit" class="price-unit">/{{ unit }}</span>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<p class="mb-0 font-weight-bold text-lg">Gestion des Employés</p>
</div>
</div>
<div class="card-body px-0 pt-0 pb-2">
<div class="table-responsive p-0">
<!-- Filter and Action Bar -->
<div
class="d-flex justify-content-between align-items-center mb-4 px-4"
>
<div class="d-flex align-items-center">
<slot name="select-filter">
<filter-table />
</slot>
</div>
<div class="d-flex align-items-center gap-2">
<slot name="employee-other-action">
<table-action />
</slot>
<slot name="employee-new-action">
<add-button text="Ajouter" @click="goToEmployee" />
</slot>
</div>
</div>
<!-- Main Content Area -->
<div class="employee-content">
<slot name="employee-table">
<!-- Default table slot - will be overridden by specific implementations -->
</slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue";
import addButton from "@/components/molecules/new-button/addButton.vue";
import { useRouter } from "vue-router";
const props = defineProps({
price: {
type: Number,
required: true
},
currency: {
type: String,
default: '€'
},
unit: {
type: String,
default: ''
},
showDecimals: {
type: Boolean,
default: true
}
});
const router = useRouter();
const formattedPrice = computed(() => {
const decimals = props.showDecimals ? 2 : 0;
return props.price.toLocaleString('fr-FR', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
});
const goToEmployee = () => {
router.push({
name: "Creation employé",
});
};
</script>
<style scoped>
.price-display {
display: flex;
align-items: baseline;
gap: 0.25rem;
}
.container-fluid {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.price-amount {
font-size: 1.5rem;
font-weight: 700;
color: #059669;
}
.card {
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
border: none;
}
.price-currency {
font-size: 1rem;
font-weight: 500;
color: #059669;
}
.card-header {
background-color: transparent;
border-bottom: 1px solid #e9ecef;
padding: 1.5rem;
}
.price-unit {
font-size: 0.875rem;
color: #6b7280;
font-weight: 400;
}
</style>
.card-body {
padding: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
}
.employee-content {
position: relative;
}
.d-flex {
display: flex;
}
.align-items-center {
align-items: center;
}
.justify-content-between {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.mb-4 {
margin-bottom: 1.5rem;
}
.px-4 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.font-weight-bold {
font-weight: 600;
}
.text-primary {
color: #5e72e4 !important;
}
@media (max-width: 768px) {
.container-fluid {
padding-left: 1rem;
padding-right: 1rem;
}
.d-flex {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.d-flex.gap-2 {
flex-direction: row;
justify-content: center;
}
}
</style>

View File

@ -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.

View File

@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreEmployeeRequest;
use App\Http\Requests\UpdateEmployeeRequest;
use App\Http\Resources\Employee\EmployeeResource;
use App\Http\Resources\Employee\EmployeeCollection;
use App\Repositories\EmployeeRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class EmployeeController extends Controller
{
public function __construct(
private readonly EmployeeRepositoryInterface $employeeRepository
) {
}
/**
* Display a listing of employees (paginated).
*/
public function index(Request $request): JsonResponse
{
try {
$perPage = (int) $request->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);
}
}
}

View File

@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePractitionerDocumentRequest;
use App\Http\Requests\UpdatePractitionerDocumentRequest;
use App\Http\Resources\Employee\PractitionerDocumentResource;
use App\Http\Resources\Employee\PractitionerDocumentCollection;
use App\Repositories\PractitionerDocumentRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class PractitionerDocumentController extends Controller
{
public function __construct(
private readonly PractitionerDocumentRepositoryInterface $practitionerDocumentRepository
) {
}
/**
* Display a listing of practitioner documents.
*/
public function index(Request $request): PractitionerDocumentCollection|JsonResponse
{
try {
$filters = [
'search' => $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);
}
}
}

View File

@ -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([

View File

@ -0,0 +1,322 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreThanatopractitionerRequest;
use App\Http\Requests\UpdateThanatopractitionerRequest;
use App\Http\Resources\Employee\ThanatopractitionerResource;
use App\Http\Resources\Employee\ThanatopractitionerCollection;
use App\Repositories\ThanatopractitionerRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ThanatopractitionerController extends Controller
{
public function __construct(
private readonly ThanatopractitionerRepositoryInterface $thanatopractitionerRepository
) {
}
/**
* Display a listing of thanatopractitioners (paginated).
*/
public function index(Request $request): JsonResponse
{
try {
$perPage = (int) $request->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);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreEmployeeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
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<string, string>
*/
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.',
];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePractitionerDocumentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
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<string, string>
*/
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.',
];
}
}

View File

@ -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.',

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreThanatopractitionerRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
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<string, string>
*/
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.',
];
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateEmployeeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
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<string, string>
*/
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.',
];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePractitionerDocumentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
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<string, string>
*/
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.',
];
}
}

View File

@ -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.',

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateThanatopractitionerRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
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<string, string>
*/
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.',
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EmployeeCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<int|string, mixed>
*/
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,
];
}),
];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\JsonResource;
class EmployeeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
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)
),
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PractitionerDocumentCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<int|string, mixed>
*/
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,
];
}),
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\JsonResource;
class PractitionerDocumentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
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)
),
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ThanatopractitionerCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<int|string, mixed>
*/
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,
];
}),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\JsonResource;
class ThanatopractitionerResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
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)
),
];
}
}

View File

@ -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;
}),
];
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Employee extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'first_name',
'last_name',
'email',
'phone',
'job_title',
'hire_date',
'active',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
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 . '%');
});
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PractitionerDocument extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'practitioner_id',
'doc_type',
'file_id',
'issue_date',
'expiry_date',
'status',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
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();
}
}

View File

@ -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);
}
/**

View File

@ -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');
}
/**

View File

@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Thanatopractitioner extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
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<string, string>
*/
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();
}
}

View File

@ -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));
});
}
/**

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Employee;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInterface
{
public function __construct(Employee $model)
{
parent::__construct($model);
}
/**
* Get all employees with optional filtering.
*/
public function getAll(array $filters = []): Collection
{
$query = $this->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(),
];
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Employee;
use Illuminate\Support\Collection;
/**
* Contract for Employee repository operations.
*/
interface EmployeeRepositoryInterface
{
/**
* Get all employees with optional filtering.
*
* @param array<string, mixed> $filters
* @return Collection<int, Employee>
*/
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<int, Employee>
*/
public function getActive(): Collection;
/**
* Get inactive employees only.
*
* @return Collection<int, Employee>
*/
public function getInactive(): Collection;
/**
* Search employees by term.
*
* @param string $term
* @return Collection<int, Employee>
*/
public function search(string $term): Collection;
/**
* Get employees with pagination.
*
* @param int $perPage
* @return array{employees: Collection<int, Employee>, pagination: array}
*/
public function getPaginated(int $perPage = 10): array;
/**
* Get employees with their thanatopractitioner data.
*
* @return Collection<int, Employee>
*/
public function getWithThanatopractitioner(): Collection;
/**
* Get employee statistics.
*
* @return array<string, int>
*/
public function getStatistics(): array;
}

View File

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\PractitionerDocument;
use Illuminate\Database\Eloquent\Collection;
class PractitionerDocumentRepository extends BaseRepository implements PractitionerDocumentRepositoryInterface
{
public function __construct(PractitionerDocument $model)
{
parent::__construct($model);
}
/**
* Get all practitioner documents with optional filtering.
*/
public function getAll(array $filters = []): Collection
{
$query = $this->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(),
];
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\PractitionerDocument;
use Illuminate\Support\Collection;
/**
* Contract for PractitionerDocument repository operations.
*/
interface PractitionerDocumentRepositoryInterface
{
/**
* Get all practitioner documents with optional filtering.
*
* @param array<string, mixed> $filters
* @return Collection<int, PractitionerDocument>
*/
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<int, PractitionerDocument>
*/
public function getByPractitionerId(int $practitionerId): Collection;
/**
* Get documents by type.
*
* @param string $docType
* @return Collection<int, PractitionerDocument>
*/
public function getByDocumentType(string $docType): Collection;
/**
* Get valid documents (not expired).
*
* @return Collection<int, PractitionerDocument>
*/
public function getValid(): Collection;
/**
* Get expired documents.
*
* @return Collection<int, PractitionerDocument>
*/
public function getExpired(): Collection;
/**
* Get documents with pagination.
*
* @param int $perPage
* @return array{documents: Collection<int, PractitionerDocument>, pagination: array}
*/
public function getPaginated(int $perPage = 10): array;
/**
* Get document statistics.
*
* @return array<string, int>
*/
public function getStatistics(): array;
}

View File

@ -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);
}

View File

@ -1,138 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class ProductRepository extends BaseRepository implements ProductRepositoryInterface
{
public function __construct(Product $model)
{
parent::__construct($model);
}
/**
* Get paginated products with filters
*/
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
{
$query = $this->model->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,
];
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Thanatopractitioner;
use Illuminate\Database\Eloquent\Collection;
class ThanatopractitionerRepository extends BaseRepository implements ThanatopractitionerRepositoryInterface
{
public function __construct(Thanatopractitioner $model)
{
parent::__construct($model);
}
/**
* Get all thanatopractitioners with optional filtering.
*/
public function getAll(array $filters = []): Collection
{
$query = $this->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(),
];
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Thanatopractitioner;
use Illuminate\Support\Collection;
/**
* Contract for Thanatopractitioner repository operations.
*/
interface ThanatopractitionerRepositoryInterface
{
/**
* Get all thanatopractitioners with optional filtering.
*
* @param array<string, mixed> $filters
* @return Collection<int, Thanatopractitioner>
*/
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<int, Thanatopractitioner>
*/
public function getWithValidAuthorization(): Collection;
/**
* Get thanatopractitioners with expired authorization.
*
* @return Collection<int, Thanatopractitioner>
*/
public function getWithExpiredAuthorization(): Collection;
/**
* Get thanatopractitioners with their complete data.
*
* @return Collection<int, Thanatopractitioner>
*/
public function getWithRelations(): Collection;
/**
* Get thanatopractitioners with pagination.
*
* @param int $perPage
* @return array{thanatopractitioners: Collection<int, Thanatopractitioner>, pagination: array}
*/
public function getPaginated(int $perPage = 10): array;
/**
* Get thanatopractitioner statistics.
*
* @return array<string, int>
*/
public function getStatistics(): array;
}

View File

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\Product;
use App\Models\ProductCategory;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
// First, we need to create a new column for categorie_id
$table->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');
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('employees', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('thanatopractitioners', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('practitioner_documents', function (Blueprint $table) {
$table->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');
}
};

View File

@ -21,5 +21,7 @@ class DatabaseSeeder extends Seeder
]);
$this->call(ProductCategorySeeder::class);
$this->call(EmployeeSeeder::class);
$this->call(ThanatopractitionerSeeder::class);
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Employee;
class EmployeeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create employees with different roles (matching actual table columns)
$employees = [
[
'first_name' => '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,
]);
}
}

View File

@ -0,0 +1,277 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Thanatopractitioner;
use App\Models\PractitionerDocument;
class ThanatopractitionerSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// First, we need to create employees that will become thanatopractitioners
$employees = [
[
'first_name' => '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
]));
}
}
}

View File

@ -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']);
});

View File

@ -1,7 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('/{any}', function () {
return file_get_contents(public_path('index.html'));
})->where('any', '.*');