Compare commits

...

33 Commits

Author SHA1 Message Date
Nyavokevin
f9b6e5e0f6 defunt Doc 2025-12-02 17:35:40 +03:00
Nyavokevin
23bce2abcf Attacher des fichiers sur les internvetions 2025-12-01 17:02:01 +03:00
Nyavokevin
496b427e13 ajout table thanato dans intervention 2025-11-26 17:53:17 +03:00
Nyavokevin
a51e05559a assigner thanato 2025-11-21 17:26:43 +03:00
Nyavokevin
4b7e075918 add multi 2025-11-14 17:13:37 +03:00
Nyavokevin
98d1743def add calendar 2025-11-13 17:47:52 +03:00
Nyavokevin
69fbe1a7a1 intervention 2025-11-12 16:44:12 +03:00
Nyavokevin
7570f46658 intervention 2025-11-11 17:45:58 +03:00
Nyavokevin
0c4ff92fd5 add defunt et intervention 2025-11-10 17:43:18 +03:00
Nyavokevin
51ff282d5b deete unused component 2025-11-07 17:05:10 +03:00
Nyavokevin
bbf60fb380 thanato 2025-11-07 16:51:09 +03:00
Nyavokevin
8d1d65e27b add gestion thanato 2025-11-06 15:09:40 +03:00
Nyavokevin
e55cc5253e add back: 2025-11-05 17:09:12 +03:00
Nyavokevin
0ea8f1866b add employee 2025-11-05 17:08:08 +03:00
Nyavokevin
aa306f5d19 add api produit categorie, et page categori produit 2025-11-04 16:43:59 +03:00
Nyavokevin
638af78e1f update produit details 2025-11-03 15:36:07 +03:00
Nyavokevin
18f9d83e5a remodifier productDetails 2025-11-03 14:57:45 +03:00
Nyavokevin
cfdbc11b1a add product detail pages 2025-11-03 13:02:11 +03:00
Nyavokevin
edb9c87c1e add product 2025-10-31 15:19:04 +03:00
Nyavokevin
4b056038d6 add notification et crud fournisseur 2025-10-29 17:17:50 +03:00
Nyavokevin
ca09f6da2f Ajout et liste fournisseur 2025-10-28 18:03:44 +03:00
Nyavokevin
e924c4f819 add page fournisseur 2025-10-28 15:25:04 +03:00
Nyavokevin
425d2d510c dynamic table 2025-10-24 10:57:52 +03:00
Nyavokevin
ea2b687533 ajout activity 2025-10-22 14:42:46 +03:00
Nyavokevin
2a1de6f384 fix modal 2025-10-21 18:01:08 +03:00
Nyavokevin
99d88ca30b fix build 2025-10-21 12:44:12 +03:00
Nyavokevin
b62cb3d717 add location 2025-10-21 12:38:07 +03:00
Nyavokevin
78700a3c5a add location 2025-10-21 12:37:36 +03:00
Nyavokevin
e2cb4499bb detail client 2025-10-20 15:58:25 +03:00
Nyavokevin
98420a29b5 contact CRUD 2025-10-16 17:29:31 +03:00
Nyavokevin
c5a4fcc546 add client detail 2025-10-10 19:00:12 +03:00
Nyavokevin
175446adbe client liste et formulaire 2025-10-09 18:25:02 +03:00
Nyavokevin
215f4c4071 add CRUD Api client, client localisation, crud contact, 2025-10-07 18:48:08 +03:00
458 changed files with 66355 additions and 966 deletions

70
thanas Normal file
View File

@ -0,0 +1,70 @@
<template>
<thanatopractitioner-template>
<template #thanatopractitioner-new-action>
<add-button text="Ajouter" @click="goToAdd" />
</template>
<template #select-filter>
<filter-table />
</template>
<template #thanatopractitioner-other-action>
<table-action />
</template>
<template #thanatopractitioner-table>
<thanatopractitioner-table
:data="thanatopractitionerData"
:loading="loadingData"
:pagination="pagination"
@view="goToDetails"
@delete="deleteThanatopractitioner"
@change-page="$emit('change-page', $event)"
/>
</template>
</thanatopractitioner-template>
</template>
<script setup>
import ThanatopractitionerTemplate from "@/components/templates/CRM/ThanatopractitionerTemplate.vue";
import ThanatopractitionerTable from "@/components/molecules/Thanatopractitioners/ThanatopractitionerTable.vue";
import addButton from "@/components/molecules/new-button/addButton.vue";
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue";
import { defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const emit = defineEmits(["pushDetails", "deleteThanatopractitioner", "changePage"]);
defineProps({
thanatopractitionerData: {
type: Array,
default: [],
},
loadingData: {
type: Boolean,
default: false,
},
pagination: {
type: Object,
default: () => ({
current_page: 1,
per_page: 10,
total: 0,
last_page: 1,
}),
},
});
const goToAdd = () => {
router.push({
name: "Creation thanatopractitioner",
});
};
const goToDetails = (thanatopractitioner) => {
emit("pushDetails", thanatopractitioner);
};
const deleteThanatopractitioner = (thanatopractitioner) => {
emit("deleteThanatopractitioner", thanatopractitioner);
};
</script>

60
thanasoft Normal file
View File

@ -0,0 +1,60 @@
<template>
<ul class="nav nav-pills flex-column">
<TabNavigationItem
icon="fas fa-eye"
label="Aperçu"
:is-active="activeTab === 'overview'"
spacing=""
@click="$emit('change-tab', 'overview')"
/>
<TabNavigationItem
icon="fas fa-info-circle"
label="Informations"
:is-active="activeTab === 'info'"
@click="$emit('change-tab', 'info')"
/>
<TabNavigationItem
icon="fas fa-calendar"
label="Agenda"
:is-active="activeTab === 'agenda'"
@click="$emit('change-tab', 'agenda')"
/>
<TabNavigationItem
icon="fas fa-info-circle"
label="Activités récentes"
:is-active="activeTab === 'activity'"
@click="$emit('change-tab', 'activity')"
/>
<TabNavigationItem
icon="fas fa-folder"
label="Documents"
:is-active="activeTab === 'documents'"
:badge="documentsCount > 0 ? documentsCount : null"
@click="$emit('change-tab', 'documents')"
/>
<TabNavigationItem
icon="fas fa-sticky-note"
label="Notes"
:is-active="activeTab === 'notes'"
@click="$emit('change-tab', 'notes')"
/>
</ul>
</template>
<script setup>
import TabNavigationItem from "@/components/atoms/client/TabNavigationItem.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
activeTab: {
type: String,
required: true,
},
documentsCount: {
type: Number,
default: 0,
},
});
defineEmits(["change-tab"]);
</script>

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,411 @@
# File Management API Documentation
## Overview
The File Management API provides a complete CRUD system for handling file uploads with organized storage, metadata management, and file organization by categories and clients.
## Base URL
```
/api/files
```
## Authentication
All file routes require authentication using Sanctum tokens.
## File Organization Structure
Files are automatically organized in storage following this structure:
```
storage/app/public/
├── client/{client_id}/{category}/{subcategory}/filename.pdf
├── client/{client_id}/devis/DEVIS_2024_12_01_10_30_45.pdf
├── client/{client_id}/facture/FACT_2024_12_01_10_30_45.pdf
└── general/{category}/{subcategory}/filename.pdf
```
### Supported Categories
- `devis` - Quotes/Devis
- `facture` - Invoices/Factures
- `contrat` - Contracts
- `document` - General Documents
- `image` - Images
- `autre` - Other files
## Endpoints Overview
### 1. List All Files
```http
GET /api/files
```
**Parameters:**
- `per_page` (optional): Number of files per page (default: 15)
- `search` (optional): Search by filename
- `mime_type` (optional): Filter by MIME type
- `uploaded_by` (optional): Filter by uploader user ID
- `category` (optional): Filter by category
- `client_id` (optional): Filter by client ID
- `date_from` (optional): Filter files uploaded after date (YYYY-MM-DD)
- `date_to` (optional): Filter files uploaded before date (YYYY-MM-DD)
- `sort_by` (optional): Sort field (default: uploaded_at)
- `sort_direction` (optional): Sort direction (default: desc)
**Response:**
```json
{
"data": [
{
"id": 1,
"file_name": "document.pdf",
"mime_type": "application/pdf",
"size_bytes": 1024000,
"size_formatted": "1000.00 KB",
"extension": "pdf",
"storage_uri": "client/123/devis/DEVIS_2024_12_01_10_30_45.pdf",
"organized_path": "client/123/devis/DEVIS_2024_12_01_10_30_45.pdf",
"sha256": "abc123...",
"uploaded_by": 1,
"uploader_name": "John Doe",
"uploaded_at": "2024-12-01 10:30:45",
"is_image": false,
"is_pdf": true,
"category": "devis",
"subcategory": "general"
}
],
"pagination": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 15,
"to": 15,
"total": 75,
"has_more_pages": true
},
"summary": {
"total_files": 15,
"total_size": 15360000,
"total_size_formatted": "14.65 MB",
"categories": {
"devis": {
"count": 8,
"total_size_formatted": "8.24 MB"
},
"facture": {
"count": 7,
"total_size_formatted": "6.41 MB"
}
}
}
}
```
### 2. Upload File
```http
POST /api/files
```
**Content-Type:** `multipart/form-data`
**Parameters:**
- `file` (required): The file to upload (max 10MB)
- `file_name` (optional): Custom filename (uses original name if not provided)
- `category` (required): File category (devis|facture|contrat|document|image|autre)
- `client_id` (optional): Associated client ID
- `subcategory` (optional): Subcategory for better organization
- `description` (optional): File description (max 500 chars)
- `tags` (optional): Array of tags (max 10 tags, 50 chars each)
- `is_public` (optional): Whether file is publicly accessible
**Example Request:**
```bash
curl -X POST \
http://localhost/api/files \
-H "Authorization: Bearer {token}" \
-F "file=@/path/to/document.pdf" \
-F "category=devis" \
-F "client_id=123" \
-F "subcategory=annual" \
-F "description=Annual quote for client" \
-F 'tags[]=quote' \
-F 'tags[]=annual'
```
**Success Response (201):**
```json
{
"data": {
"id": 1,
"file_name": "document.pdf",
"mime_type": "application/pdf",
"size_bytes": 1024000,
"storage_uri": "client/123/devis/annual/document_2024_12_01_10_30_45.pdf",
"category": "devis",
"subcategory": "annual",
"uploaded_at": "2024-12-01 10:30:45"
},
"message": "Fichier téléchargé avec succès."
}
```
### 3. Get File Details
```http
GET /api/files/{id}
```
**Response:** Same as file object in list endpoint
### 4. Update File Metadata
```http
PUT /api/files/{id}
```
**Parameters:**
- `file_name` (optional): New filename
- `description` (optional): Updated description
- `tags` (optional): Updated tags array
- `is_public` (optional): Updated public status
- `category` (optional): Move to new category
- `client_id` (optional): Change associated client
- `subcategory` (optional): Update subcategory
**Note:** If category, client_id, or subcategory are changed, the file will be automatically moved to the new location.
### 5. Delete File
```http
DELETE /api/files/{id}
```
**Response:**
```json
{
"message": "Fichier supprimé avec succès."
}
```
### 6. Get Files by Category
```http
GET /api/files/by-category/{category}
```
**Parameters:** Same as list endpoint with additional `per_page`
### 7. Get Files by Client
```http
GET /api/files/by-client/{clientId}
```
**Parameters:** Same as list endpoint with additional `per_page`
### 8. Get Organized File Structure
```http
GET /api/files/organized
```
**Response:**
```json
{
"data": {
"devis/general": {
"category": "devis",
"subcategory": "general",
"files": [...],
"count": 25
},
"facture/annual": {
"category": "facture",
"subcategory": "annual",
"files": [...],
"count": 15
}
},
"message": "Structure de fichiers récupérée avec succès."
}
```
### 9. Get Storage Statistics
```http
GET /api/files/statistics
```
**Response:**
```json
{
"data": {
"total_files": 150,
"total_size_bytes": 1073741824,
"total_size_formatted": "1.00 GB",
"by_type": {
"application/pdf": {
"count": 85,
"total_size": 734003200
},
"image/jpeg": {
"count": 45,
"total_size": 209715200
}
},
"by_category": {
"devis": {
"count": 60,
"total_size": 429496729
},
"facture": {
"count": 50,
"total_size": 322122547
}
}
},
"message": "Statistiques de stockage récupérées avec succès."
}
```
### 10. Generate Download URL
```http
GET /api/files/{id}/download
```
**Response:**
```json
{
"data": {
"download_url": "/storage/client/123/devis/document_2024_12_01_10_30_45.pdf",
"file_name": "document.pdf",
"mime_type": "application/pdf"
},
"message": "URL de téléchargement générée avec succès."
}
```
## Error Responses
### Validation Error (422)
```json
{
"message": "Les données fournies ne sont pas valides.",
"errors": {
"file": ["Le fichier est obligatoire."],
"category": ["La catégorie est obligatoire."]
}
}
```
### Not Found (404)
```json
{
"message": "Fichier non trouvé."
}
```
### Server Error (500)
```json
{
"message": "Une erreur est survenue lors du traitement de la requête.",
"error": "Detailed error message (only in debug mode)"
}
```
## File Features
### Automatic Organization
- Files are automatically organized by category and client
- Timestamps are added to prevent filename conflicts
- Safe slug generation for subcategories
### Security
- SHA256 hash calculation for file integrity
- User-based access control
- File size validation (10MB limit)
### Metadata Support
- MIME type detection
- File size tracking
- Upload timestamp
- User attribution
- Custom tags and descriptions
### Storage Management
- Public storage disk usage
- Efficient path organization
- Duplicate prevention through hashing
- Automatic file moving on metadata updates
## Usage Examples
### Upload a Quote for Client
```bash
curl -X POST \
http://localhost/api/files \
-H "Authorization: Bearer {token}" \
-F "file=@quote_2024.pdf" \
-F "category=devis" \
-F "client_id=123" \
-F "subcategory=annual_2024" \
-F 'tags[]=quote' \
-F 'tags[]=annual'
```
### Get All Client Files
```bash
curl -X GET \
"http://localhost/api/files/by-client/123?per_page=20&sort_by=uploaded_at&sort_direction=desc" \
-H "Authorization: Bearer {token}"
```
### Get File Statistics
```bash
curl -X GET \
"http://localhost/api/files/statistics" \
-H "Authorization: Bearer {token}"
```
### Search Files
```bash
curl -X GET \
"http://localhost/api/files?search=annual&category=devis" \
-H "Authorization: Bearer {token}"
```
## Notes
- All file operations are logged for audit purposes
- Files are stored in `storage/app/public/` directory
- The system automatically handles file moving when categories change
- Download URLs are generated on-demand for security
- Pagination is applied to all list endpoints
- French language is used for all API messages and validations

View File

@ -0,0 +1,219 @@
# Intervention with All Data - Implementation Summary
## Overview
Created a comprehensive `createInterventionalldata` method in the InterventionController that handles creating an intervention along with all related entities (deceased, client, contact, location, documents) in a single atomic transaction.
## Files Created/Modified
### 1. Form Request
**File**: `app/Http/Requests/StoreInterventionWithAllDataRequest.php`
- Comprehensive validation for all entities
- Step-level error grouping (deceased, client, contact, location, documents, intervention)
- French error messages
- File validation for document uploads
### 2. Controller Method
**File**: `app/Http/Controllers/Api/InterventionController.php`
- Added `createInterventionalldata()` method
- Database transaction wrapping all operations
- Step-by-step creation flow:
1. Create deceased
2. Create client
3. Create contact (if provided)
4. Prepare location data
5. Create intervention
6. Handle document uploads
- Automatic rollback on any error
- Comprehensive logging
### 3. API Route
**File**: `routes/api.php`
- Added endpoint: `POST /api/interventions/with-all-data`
- Protected by authentication middleware
### 4. Repository Improvements
**Files**:
- `app/Repositories/DeceasedRepositoryInterface.php` (created)
- `app/Repositories/DeceasedRepository.php` (updated)
**Changes**:
- DeceasedRepository now extends BaseRepository
- Implements BaseRepositoryInterface
- Inherits transaction support from BaseRepository
- Consistent with other repository implementations
## API Endpoint
### POST /api/interventions/with-all-data
#### Request Structure:
```json
{
"deceased": {
"last_name": "Required",
"first_name": "Optional",
"birth_date": "Optional",
"death_date": "Optional",
"place_of_death": "Optional",
"notes": "Optional"
},
"client": {
"name": "Required",
"vat_number": "Optional",
"siret": "Optional",
"email": "Optional",
"phone": "Optional",
"billing_address_line1": "Optional",
"billing_postal_code": "Optional",
"billing_city": "Optional",
"billing_country_code": "Optional",
"notes": "Optional",
"is_active": "Optional"
},
"contact": {
"first_name": "Optional",
"last_name": "Optional",
"email": "Optional",
"phone": "Optional",
"role": "Optional"
},
"location": {
"name": "Optional",
"address": "Optional",
"city": "Optional",
"postal_code": "Optional",
"country_code": "Optional",
"access_instructions": "Optional",
"notes": "Optional"
},
"documents": [
{
"file": "File upload",
"name": "Required",
"description": "Optional"
}
],
"intervention": {
"type": "Required",
"scheduled_at": "Optional",
"duration_min": "Optional",
"status": "Optional",
"assigned_practitioner_id": "Optional",
"order_giver": "Optional",
"notes": "Optional",
"created_by": "Optional"
}
}
```
#### Success Response (201):
```json
{
"message": "Intervention créée avec succès",
"data": {
"intervention": { ... },
"deceased": { ... },
"client": { ... },
"contact_id": 123,
"documents_count": 2
}
}
```
#### Error Response (422 - Validation):
```json
{
"message": "Données invalides",
"errors": {
"deceased": ["Le nom de famille est obligatoire."],
"client": ["Le nom du client est obligatoire."],
"intervention": ["Le type d'intervention est obligatoire."]
}
}
```
## Transaction Flow
```
DB::beginTransaction()
1. Create Deceased
2. Create Client
3. Create Contact (if provided)
4. Prepare Location Notes
5. Create Intervention
6. Handle Document Uploads (pending implementation)
DB::commit()
```
## Error Handling
### Validation Errors
- Caught separately with proper HTTP status (422)
- Grouped by step for better UX
- French error messages
### General Errors
- Caught with proper HTTP status (500)
- Full exception logged with trace
- Input data logged (excluding files)
- Transaction automatically rolled back
## Data Integrity
1. **BaseRepository Transaction Support**: All repository create operations use transactions
2. **Controller-level Transaction**: Main transaction wraps all operations
3. **Automatic Rollback**: Any exception triggers automatic rollback
4. **Validation Before Transaction**: All data validated before any DB operations
## Benefits
1. **Atomicity**: All-or-nothing operation - prevents partial data creation
2. **Data Integrity**: No orphaned records if any step fails
3. **Performance**: Single HTTP request for complex operation
4. **User Experience**: One form instead of multiple steps
5. **Validation**: Comprehensive client and server-side validation
6. **Logging**: Full audit trail for debugging
## Next Steps
1. **Document Upload Implementation**: Complete file upload and storage logic
2. **Location Model**: Consider creating a proper Location model instead of notes
3. **Client Location Association**: Link interventions to actual locations
4. **File Storage**: Implement proper file storage with intervention folders
5. **Email Notifications**: Add notifications to relevant parties
6. **Audit Trail**: Add more detailed logging for compliance
## Testing
To test the endpoint:
```bash
# Create intervention with all data
curl -X POST http://localhost/api/interventions/with-all-data \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d @intervention-data.json
```
## Notes
- All date fields should use ISO format (Y-m-d H:i:s)
- Document files should be sent as multipart/form-data
- Location data is currently appended to intervention notes (can be enhanced)
- Contact creation is optional - only created if data provided
- Default status is "demande" if not specified

View File

@ -0,0 +1,153 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientCategoryRequest;
use App\Http\Resources\Client\ClientCategoryResource;
use App\Http\Resources\ClientResource;
use App\Models\ClientCategory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class ClientCategoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): AnonymousResourceCollection
{
$query = ClientCategory::query();
$categories = $query->get();
return ClientCategoryResource::collection($categories);
}
/**
* Store a newly created resource in storage.
*/
public function store(ClientCategoryRequest $request): ClientCategoryResource
{
$category = ClientCategory::create($request->validated());
return new ClientCategoryResource($category);
}
/**
* Display the specified resource.
*/
public function show(ClientCategory $clientCategory): ClientCategoryResource
{
return new ClientCategoryResource($clientCategory);
}
/**
* Display the specified resource by slug.
*/
public function showBySlug(string $slug): ClientCategoryResource
{
$category = ClientCategory::where('slug', $slug)->firstOrFail();
return new ClientCategoryResource($category);
}
/**
* Update the specified resource in storage.
*/
public function update(ClientCategoryRequest $request, ClientCategory $clientCategory): ClientCategoryResource
{
$clientCategory->update($request->validated());
return new ClientCategoryResource($clientCategory->fresh());
}
/**
* Remove the specified resource from storage.
*/
public function destroy(ClientCategory $clientCategory): JsonResponse
{
// Check if category has clients
if ($clientCategory->clients()->exists()) {
return response()->json([
'success' => false,
'message' => 'Cannot delete category that has clients assigned. Please reassign clients first.'
], 422);
}
$clientCategory->delete();
return response()->json([
'success' => true,
'message' => 'Client category deleted successfully.'
]);
}
/**
* Toggle active status of the category.
*/
public function toggleStatus(ClientCategory $clientCategory, Request $request): ClientCategoryResource
{
$request->validate([
'is_active' => 'required|boolean',
]);
$clientCategory->update([
'is_active' => $request->boolean('is_active'),
]);
return new ClientCategoryResource($clientCategory->fresh());
}
/**
* Get clients for a specific category.
*/
public function clients(ClientCategory $clientCategory, Request $request): AnonymousResourceCollection
{
$query = $clientCategory->clients();
// Active status filter
if ($request->has('is_active')) {
$query->where('is_active', $request->boolean('is_active'));
}
// Pagination
$perPage = $request->get('per_page', 15);
$clients = $query->paginate($perPage);
return ClientResource::collection($clients);
}
/**
* Reorder categories.
*/
public function reorder(Request $request): JsonResponse
{
$request->validate([
'order' => 'required|array',
'order.*' => 'integer|exists:client_categories,id',
]);
foreach ($request->order as $index => $categoryId) {
ClientCategory::where('id', $categoryId)->update(['sort_order' => $index]);
}
return response()->json([
'success' => true,
'message' => 'Categories reordered successfully.'
]);
}
/**
* Get all active categories for dropdowns.
*/
public function active(): AnonymousResourceCollection
{
$categories = ClientCategory::where('is_active', true)
->orderBy('sort_order', 'asc')
->orderBy('name', 'asc')
->get();
return ClientCategoryResource::collection($categories);
}
}

View File

@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientRequest;
use App\Http\Requests\UpdateClientRequest;
use App\Http\Resources\Client\ClientResource;
use App\Http\Resources\Client\ClientCollection;
use App\Repositories\ClientRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class ClientController extends Controller
{
public function __construct(
private readonly ClientRepositoryInterface $clientRepository
) {
}
/**
* Display a listing of clients.
*/
public function index(Request $request): ClientCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'is_active' => $request->get('is_active'),
'group_id' => $request->get('group_id'),
'client_category_id' => $request->get('client_category_id'),
'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 !== '';
});
$clients = $this->clientRepository->paginate($perPage, $filters);
return new ClientCollection($clients);
} catch (\Exception $e) {
Log::error('Error fetching clients: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created client.
*/
public function store(StoreClientRequest $request): ClientResource|JsonResponse
{
try {
$client = $this->clientRepository->create($request->validated());
return new ClientResource($client);
} catch (\Exception $e) {
Log::error('Error creating client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client.
*/
public function show(string $id): ClientResource|JsonResponse
{
try {
$client = $this->clientRepository->find($id);
if (!$client) {
return response()->json([
'message' => 'Client non trouvé.',
], 404);
}
return new ClientResource($client);
} catch (\Exception $e) {
Log::error('Error fetching client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function searchBy(Request $request): JsonResponse
{
try {
$name = $request->get('name', '');
if (empty($name)) {
return response()->json([
'message' => 'Le paramètre "name" est requis.',
], 400);
}
$clients = $this->clientRepository->searchByName($name);
return response()->json([
'data' => $clients,
'count' => $clients->count(),
'message' => $clients->count() > 0
? 'Clients trouvés avec succès.'
: 'Aucun client trouvé.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching clients by name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'search_term' => $name,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified client.
*/
public function update(UpdateClientRequest $request, string $id): ClientResource|JsonResponse
{
try {
$updated = $this->clientRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Client non trouvé ou échec de la mise à jour.',
], 404);
}
$client = $this->clientRepository->find($id);
return new ClientResource($client);
} catch (\Exception $e) {
Log::error('Error updating client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified client.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->clientRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Client non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Client supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientGroupRequest;
use App\Http\Requests\UpdateClientGroupRequest;
use App\Http\Resources\Client\ClientGroupResource;
use App\Repositories\ClientGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class ClientGroupController extends Controller
{
public function __construct(
private readonly ClientGroupRepositoryInterface $clientGroupRepository
) {
}
/**
* Display a listing of client groups.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$clientGroups = $this->clientGroupRepository->all();
return ClientGroupResource::collection($clientGroups);
} catch (\Exception $e) {
Log::error('Error fetching client groups: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des groupes de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created client group.
*/
public function store(StoreClientGroupRequest $request): ClientGroupResource|JsonResponse
{
try {
$clientGroup = $this->clientGroupRepository->create($request->validated());
return new ClientGroupResource($clientGroup);
} catch (\Exception $e) {
Log::error('Error creating client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client group.
*/
public function show(string $id): ClientGroupResource|JsonResponse
{
try {
$clientGroup = $this->clientGroupRepository->find($id);
if (!$clientGroup) {
return response()->json([
'message' => 'Groupe de clients non trouvé.',
], 404);
}
return new ClientGroupResource($clientGroup);
} catch (\Exception $e) {
Log::error('Error fetching client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_group_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified client group.
*/
public function update(UpdateClientGroupRequest $request, string $id): ClientGroupResource|JsonResponse
{
try {
$updated = $this->clientGroupRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Groupe de clients non trouvé ou échec de la mise à jour.',
], 404);
}
$clientGroup = $this->clientGroupRepository->find($id);
return new ClientGroupResource($clientGroup);
} catch (\Exception $e) {
Log::error('Error updating client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_group_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified client group.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->clientGroupRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Groupe de clients non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Groupe de clients supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting client group: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_group_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du groupe de clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientLocationRequest;
use App\Http\Requests\UpdateClientLocationRequest;
use App\Http\Resources\Client\ClientLocationResource;
use App\Http\Resources\Client\ClientLocationCollection;
use App\Repositories\ClientLocationRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class ClientLocationController extends Controller
{
public function __construct(
private readonly ClientLocationRepositoryInterface $clientLocationRepository
) {
}
/**
* Display a listing of client locations.
*/
public function index(Request $request)
{
try {
$filters = $request->only(['client_id', 'is_default', 'search']);
$perPage = $request->get('per_page', 10);
$clientLocations = $this->clientLocationRepository->getPaginated($filters, $perPage);
return new ClientLocationCollection($clientLocations);
} catch (\Exception $e) {
Log::error('Error fetching client locations: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des lieux clients.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created client location.
*/
public function store(StoreClientLocationRequest $request): ClientLocationResource|JsonResponse
{
try {
$clientLocation = $this->clientLocationRepository->create($request->validated());
return new ClientLocationResource($clientLocation);
} catch (\Exception $e) {
Log::error('Error creating client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client id.
*/
public function getLocationsByClient(string $id)
{
try {
$clientLocations = $this->clientLocationRepository->getByClientId((int)$id);
return ClientLocationResource::collection($clientLocations);
} catch (\Exception $e) {
Log::error('Error fetching client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified client location.
*/
public function show(string $id): ClientLocationResource|JsonResponse
{
try {
$clientLocation = $this->clientLocationRepository->find($id);
if (!$clientLocation) {
return response()->json([
'message' => 'Lieu client non trouvé.',
], 404);
}
return new ClientLocationResource($clientLocation);
} catch (\Exception $e) {
Log::error('Error fetching client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified client location.
*/
public function update(UpdateClientLocationRequest $request, string $id): ClientLocationResource|JsonResponse
{
try {
$updated = $this->clientLocationRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Lieu client non trouvé ou échec de la mise à jour.',
], 404);
}
$clientLocation = $this->clientLocationRepository->find($id);
return new ClientLocationResource($clientLocation);
} catch (\Exception $e) {
Log::error('Error updating client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified client location.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->clientLocationRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Lieu client non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Lieu client supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting client location: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_location_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du lieu client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreContactRequest;
use App\Http\Requests\UpdateContactRequest;
use App\Http\Resources\Contact\ContactResource;
use App\Http\Resources\Contact\ContactCollection;
use App\Repositories\ContactRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class ContactController extends Controller
{
public function __construct(
private readonly ContactRepositoryInterface $contactRepository
) {
}
/**
* Display a listing of contacts.
*/
public function index(): ContactCollection
{
try {
$perPage = request('per_page', 15);
$filters = [
'search' => request('search'),
'is_primary' => request('is_primary'),
'client_id' => request('client_id'),
'sort_by' => request('sort_by', 'created_at'),
'sort_direction' => request('sort_direction', 'desc'),
];
$contacts = $this->contactRepository->paginate($perPage, $filters);
return new ContactCollection($contacts);
} catch (\Exception $e) {
Log::error('Error fetching contacts: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des contacts.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created contact.
*/
public function store(StoreContactRequest $request): ContactResource|JsonResponse
{
try {
$contact = $this->contactRepository->create($request->validated());
return new ContactResource($contact);
} catch (\Exception $e) {
Log::error('Error creating contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified contact.
*/
public function show(string $id): ContactResource|JsonResponse
{
try {
$contact = $this->contactRepository->find($id);
if (!$contact) {
return response()->json([
'message' => 'Contact non trouvé.',
], 404);
}
return new ContactResource($contact);
} catch (\Exception $e) {
Log::error('Error fetching contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'contact_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified contact.
*/
public function update(UpdateContactRequest $request, string $id): ContactResource|JsonResponse
{
try {
$updated = $this->contactRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Contact non trouvé ou échec de la mise à jour.',
], 404);
}
$contact = $this->contactRepository->find($id);
return new ContactResource($contact);
} catch (\Exception $e) {
Log::error('Error updating contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'contact_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified contact.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->contactRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Contact non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Contact supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting contact: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'contact_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du contact.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function getContactsByClient(string $clientId): JsonResponse
{
try {
$intId = (int) $clientId;
$contacts = $this->contactRepository->getByClientId($intId);
return response()->json([
'data' => ContactResource::collection($contacts),
], 200);
} catch (\Exception $e) {
Log::error('Error fetching contacts by client: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $clientId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des contacts du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function getContactsByFournisseur(string $fournisseurId): JsonResponse
{
try {
$intId = (int) $fournisseurId;
$contacts = $this->contactRepository->getByFournisseurId($intId);
return response()->json([
'data' => ContactResource::collection($contacts),
], 200);
} catch (\Exception $e) {
Log::error('Error fetching contacts by fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $fournisseurId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des contacts du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreDeceasedRequest;
use App\Http\Requests\UpdateDeceasedRequest;
use App\Http\Resources\Deceased\DeceasedResource;
use App\Http\Resources\Deceased\DeceasedCollection;
use App\Models\Deceased;
use App\Repositories\DeceasedRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class DeceasedController extends Controller
{
/**
* @var DeceasedRepositoryInterface
*/
protected $deceasedRepository;
/**
* DeceasedController constructor.
*
* @param DeceasedRepositoryInterface $deceasedRepository
*/
public function __construct(DeceasedRepositoryInterface $deceasedRepository)
{
$this->deceasedRepository = $deceasedRepository;
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
try {
$filters = $request->only([
'search',
'start_date',
'end_date',
'sort_by',
'sort_order'
]);
$perPage = $request->input('per_page', 15);
$deceased = $this->deceasedRepository->getAllPaginated($filters, $perPage);
return response()->json(new DeceasedCollection($deceased));
} catch (\Exception $e) {
Log::error('Error fetching deceased list: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des défunts.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreDeceasedRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$deceased = $this->deceasedRepository->create($validated);
return response()->json(new DeceasedResource($deceased), Response::HTTP_CREATED);
} catch (\Exception $e) {
Log::error('Error creating deceased: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la création du défunt.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Display the specified resource.
*/
public function show(int $id): JsonResponse
{
try {
$deceased = $this->deceasedRepository->findById($id);
return response()->json(new DeceasedResource($deceased));
} catch (\Exception $e) {
Log::error('Error fetching deceased details: ' . $e->getMessage());
return response()->json([
'message' => 'Défunt non trouvé ou une erreur est survenue.',
'error' => $e->getMessage()
], Response::HTTP_NOT_FOUND);
}
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateDeceasedRequest $request, int $id): JsonResponse
{
try {
$deceased = $this->deceasedRepository->findById($id);
$validated = $request->validated();
$updatedDeceased = $this->deceasedRepository->update($deceased, $validated);
return response()->json(new DeceasedResource($updatedDeceased));
} catch (\Exception $e) {
Log::error('Error updating deceased: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du défunt.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy(int $id): JsonResponse
{
try {
$deceased = $this->deceasedRepository->findById($id);
$this->deceasedRepository->delete($deceased);
return response()->json(null, Response::HTTP_NO_CONTENT);
} catch (\Exception $e) {
Log::error('Error deleting deceased: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du défunt.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Search deceased by name or other criteria.
*/
public function searchBy(Request $request): JsonResponse
{
try {
$search = $request->get('search', '');
if (empty($search)) {
return response()->json([
'message' => 'Le paramètre "search" est requis.',
], 400);
}
$deceased = $this->deceasedRepository->searchByName($search);
return response()->json([
'data' => $deceased,
'count' => $deceased->count(),
'message' => $deceased->count() > 0
? 'Défunts trouvés avec succès.'
: 'Aucun défunt trouvé.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching deceased by name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'search_term' => $search ?? '',
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des défunts.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreDeceasedDocumentRequest;
use App\Http\Requests\UpdateDeceasedDocumentRequest;
use App\Http\Resources\Deceased\DeceasedDocumentResource;
use App\Http\Resources\Deceased\DeceasedDocumentCollection;
use App\Repositories\DeceasedDocumentRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class DeceasedDocumentController extends Controller
{
public function __construct(
private readonly DeceasedDocumentRepositoryInterface $deceasedDocumentRepository
) {
}
/**
* Display a listing of deceased documents.
*/
public function index(Request $request): DeceasedDocumentCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'deceased_id' => $request->get('deceased_id'),
'doc_type' => $request->get('doc_type'),
'file_id' => $request->get('file_id'),
'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->deceasedDocumentRepository->paginate($perPage, $filters);
return new DeceasedDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Erreur lors de la récupération des documents du défunt: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents du défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get documents by deceased ID.
*/
public function byDeceased(string $deceasedId): DeceasedDocumentCollection|JsonResponse
{
try {
$documents = $this->deceasedDocumentRepository->getByDeceasedId((int) $deceasedId);
return new DeceasedDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Erreur lors de la récupération des documents par défunt: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'deceased_id' => $deceasedId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents du défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get documents by document type.
*/
public function byDocType(Request $request): DeceasedDocumentCollection|JsonResponse
{
try {
$docType = $request->get('doc_type');
if (!$docType) {
return response()->json([
'message' => 'Le paramètre doc_type est requis.',
], 400);
}
$documents = $this->deceasedDocumentRepository->getByDocType($docType);
return new DeceasedDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Erreur lors de la récupération des documents par type: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'doc_type' => $request->get('doc_type'),
]);
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 documents by file ID.
*/
public function byFile(string $fileId): DeceasedDocumentCollection|JsonResponse
{
try {
$documents = $this->deceasedDocumentRepository->getByFileId((int) $fileId);
return new DeceasedDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Erreur lors de la récupération des documents par fichier: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'file_id' => $fileId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents par fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Search documents by various criteria.
*/
public function search(Request $request): DeceasedDocumentCollection|JsonResponse
{
try {
$criteria = [
'deceased_id' => $request->get('deceased_id'),
'doc_type' => $request->get('doc_type'),
'file_id' => $request->get('file_id'),
'generated_from' => $request->get('generated_from'),
'generated_to' => $request->get('generated_to'),
];
// Remove null criteria
$criteria = array_filter($criteria, function ($value) {
return $value !== null && $value !== '';
});
$documents = $this->deceasedDocumentRepository->search($criteria);
return new DeceasedDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Erreur lors de la recherche de documents: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'criteria' => $request->all(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche de documents.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created deceased document.
*/
public function store(StoreDeceasedDocumentRequest $request): DeceasedDocumentResource|JsonResponse
{
try {
$document = $this->deceasedDocumentRepository->create($request->validated());
return new DeceasedDocumentResource($document);
} catch (\Exception $e) {
Log::error('Erreur lors de la création du document du défunt: ' . $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 défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified deceased document.
*/
public function show(string $id): DeceasedDocumentResource|JsonResponse
{
try {
$document = $this->deceasedDocumentRepository->find($id);
if (!$document) {
return response()->json([
'message' => 'Document du défunt non trouvé.',
], 404);
}
return new DeceasedDocumentResource($document);
} catch (\Exception $e) {
Log::error('Erreur lors de la récupération du document du défunt: ' . $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 défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified deceased document.
*/
public function update(UpdateDeceasedDocumentRequest $request, string $id): DeceasedDocumentResource|JsonResponse
{
try {
$updated = $this->deceasedDocumentRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Document du défunt non trouvé ou échec de la mise à jour.',
], 404);
}
$document = $this->deceasedDocumentRepository->find($id);
return new DeceasedDocumentResource($document);
} catch (\Exception $e) {
Log::error('Erreur lors de la mise à jour du document du défunt: ' . $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 défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified deceased document.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->deceasedDocumentRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Document du défunt non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Document du défunt supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Erreur lors de la suppression du document du défunt: ' . $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 défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

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,438 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\FileAttachment\FileAttachmentResource;
use App\Models\File;
use App\Models\FileAttachment;
use App\Models\Intervention;
use App\Models\Client;
use App\Models\Deceased;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class FileAttachmentController extends Controller
{
/**
* Attach a file to a model (Intervention, Client, Deceased, etc.)
*/
public function attach(Request $request): JsonResponse
{
try {
$request->validate([
'file_id' => 'required|exists:files,id',
'attachable_type' => 'required|string|in:App\Models\Intervention,App\Models\Client,App\Models\Deceased',
'attachable_id' => 'required|integer',
'label' => 'nullable|string|max:255',
'sort_order' => 'nullable|integer|min:0',
]);
// Verify the attachable model exists
$attachableModel = $this->getAttachableModel($request->attachable_type, $request->attachable_id);
if (!$attachableModel) {
return response()->json([
'message' => 'Le modèle cible n\'existe pas.',
], 404);
}
// Check if file is already attached to this model
$existingAttachment = FileAttachment::where('file_id', $request->file_id)
->where('attachable_type', $request->attachable_type)
->where('attachable_id', $request->attachable_id)
->first();
if ($existingAttachment) {
return response()->json([
'message' => 'Ce fichier est déjà attaché à cet élément.',
], 422);
}
DB::beginTransaction();
try {
// Create the attachment
$attachment = FileAttachment::create([
'file_id' => $request->file_id,
'attachable_type' => $request->attachable_type,
'attachable_id' => $request->attachable_id,
'label' => $request->label,
'sort_order' => $request->sort_order ?? 0,
]);
// Load relationships for response
$attachment->load('file');
DB::commit();
return response()->json([
'data' => new FileAttachmentResource($attachment),
'message' => 'Fichier attaché avec succès.',
], 201);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error('Error attaching file: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'request_data' => $request->all(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de l\'attachement du fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Detach a file from a model
*/
public function detach(Request $request, string $attachmentId): JsonResponse
{
try {
$attachment = FileAttachment::find($attachmentId);
if (!$attachment) {
return response()->json([
'message' => 'Attachement de fichier non trouvé.',
], 404);
}
DB::beginTransaction();
try {
$attachment->delete();
DB::commit();
return response()->json([
'message' => 'Fichier détaché avec succès.',
], 200);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error('Error detaching file: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'attachment_id' => $attachmentId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors du détachement du fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update file attachment metadata
*/
public function update(Request $request, string $attachmentId): JsonResponse
{
try {
$request->validate([
'label' => 'nullable|string|max:255',
'sort_order' => 'nullable|integer|min:0',
]);
$attachment = FileAttachment::find($attachmentId);
if (!$attachment) {
return response()->json([
'message' => 'Attachement de fichier non trouvé.',
], 404);
}
DB::beginTransaction();
try {
$attachment->update($request->only(['label', 'sort_order']));
$attachment->load('file');
DB::commit();
return response()->json([
'data' => new FileAttachmentResource($attachment),
'message' => 'Attachement de fichier mis à jour avec succès.',
], 200);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error('Error updating file attachment: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'attachment_id' => $attachmentId,
'request_data' => $request->all(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de l\'attachement.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get files attached to a specific model
*/
public function getAttachedFiles(Request $request): JsonResponse
{
try {
$request->validate([
'attachable_type' => 'required|string|in:App\Models\Intervention,App\Models\Client,App\Models\Deceased',
'attachable_id' => 'required|integer',
]);
$attachments = FileAttachment::where('attachable_type', $request->attachable_type)
->where('attachable_id', $request->attachable_id)
->with('file')
->orderBy('sort_order')
->orderBy('created_at')
->get();
return response()->json([
'data' => FileAttachmentResource::collection($attachments),
'count' => $attachments->count(),
'message' => 'Fichiers attachés récupérés avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error getting attached files: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'request_data' => $request->all(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers attachés.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get files attached to an intervention
*/
public function getInterventionFiles(Request $request, int $interventionId): JsonResponse
{
try {
$intervention = Intervention::find($interventionId);
if (!$intervention) {
return response()->json([
'message' => 'Intervention non trouvée.',
], 404);
}
$attachments = $intervention->fileAttachments()->with('file')->get();
return response()->json([
'data' => FileAttachmentResource::collection($attachments),
'count' => $attachments->count(),
'message' => 'Fichiers de l\'intervention récupérés avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error getting intervention files: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'intervention_id' => $interventionId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers de l\'intervention.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get files attached to a client
*/
public function getClientFiles(Request $request, int $clientId): JsonResponse
{
try {
$client = Client::find($clientId);
if (!$client) {
return response()->json([
'message' => 'Client non trouvé.',
], 404);
}
$attachments = $client->fileAttachments()->with('file')->get();
return response()->json([
'data' => FileAttachmentResource::collection($attachments),
'count' => $attachments->count(),
'message' => 'Fichiers du client récupérés avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error getting client files: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'client_id' => $clientId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get files attached to a deceased
*/
public function getDeceasedFiles(Request $request, int $deceasedId): JsonResponse
{
try {
$deceased = Deceased::find($deceasedId);
if (!$deceased) {
return response()->json([
'message' => 'Défunt non trouvé.',
], 404);
}
$attachments = $deceased->fileAttachments()->with('file')->get();
return response()->json([
'data' => FileAttachmentResource::collection($attachments),
'count' => $attachments->count(),
'message' => 'Fichiers du défunt récupérés avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error getting deceased files: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'deceased_id' => $deceasedId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers du défunt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Detach multiple files at once
*/
public function detachMultiple(Request $request): JsonResponse
{
try {
$request->validate([
'attachment_ids' => 'required|array|min:1',
'attachment_ids.*' => 'exists:file_attachments,id',
]);
DB::beginTransaction();
try {
$deletedCount = FileAttachment::whereIn('id', $request->attachment_ids)->delete();
DB::commit();
return response()->json([
'deleted_count' => $deletedCount,
'message' => $deletedCount . ' fichier(s) détaché(s) avec succès.',
], 200);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error('Error detaching multiple files: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'attachment_ids' => $request->attachment_ids ?? [],
]);
return response()->json([
'message' => 'Une erreur est survenue lors du détachement des fichiers.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Reorder file attachments
*/
public function reorder(Request $request): JsonResponse
{
try {
$request->validate([
'attachments' => 'required|array|min:1',
'attachments.*.id' => 'required|exists:file_attachments,id',
'attachments.*.sort_order' => 'required|integer|min:0',
]);
DB::beginTransaction();
try {
foreach ($request->attachments as $attachmentData) {
FileAttachment::where('id', $attachmentData['id'])
->update(['sort_order' => $attachmentData['sort_order']]);
}
DB::commit();
return response()->json([
'message' => 'Ordre des fichiers mis à jour avec succès.',
], 200);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error('Error reordering file attachments: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'attachments' => $request->attachments ?? [],
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la réorganisation des fichiers.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get the attachable model instance
*/
private function getAttachableModel(string $type, int $id): ?Model
{
return match ($type) {
Intervention::class => Intervention::find($id),
Client::class => Client::find($id),
Deceased::class => Deceased::find($id),
default => null,
};
}
}

View File

@ -0,0 +1,444 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreFileRequest;
use App\Http\Requests\UpdateFileRequest;
use App\Http\Resources\File\FileResource;
use App\Http\Resources\File\FileCollection;
use App\Repositories\FileRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class FileController extends Controller
{
public function __construct(
private readonly FileRepositoryInterface $fileRepository
) {
}
/**
* Display a listing of files.
*/
public function index(Request $request): FileCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'mime_type' => $request->get('mime_type'),
'uploaded_by' => $request->get('uploaded_by'),
'category' => $request->get('category'),
'client_id' => $request->get('client_id'),
'date_from' => $request->get('date_from'),
'date_to' => $request->get('date_to'),
'sort_by' => $request->get('sort_by', 'uploaded_at'),
'sort_direction' => $request->get('sort_direction', 'desc'),
];
// Remove null filters
$filters = array_filter($filters, function ($value) {
return $value !== null && $value !== '';
});
$files = $this->fileRepository->paginate($perPage, $filters);
return new FileCollection($files);
} catch (\Exception $e) {
Log::error('Error fetching files: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly uploaded file.
*/
public function store(StoreFileRequest $request): FileResource|JsonResponse
{
try {
$validatedData = $request->validated();
$file = $request->file('file');
// Generate organized storage path
$storagePath = $this->generateOrganizedPath(
$validatedData['category'],
$validatedData['client_id'] ?? null,
$validatedData['subcategory'] ?? null,
$validatedData['file_name']
);
// Store the file
$storedFilePath = $file->store($storagePath, 'public');
// Calculate SHA256 hash
$hash = hash_file('sha256', $file->path());
// Prepare data for database
$fileData = array_merge($validatedData, [
'storage_uri' => $storedFilePath,
'sha256' => $hash,
'uploaded_by' => $request->user()->id,
'uploaded_at' => now(),
]);
$createdFile = $this->fileRepository->create($fileData);
return new FileResource($createdFile);
} catch (\Exception $e) {
Log::error('Error uploading file: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'user_id' => $request->user()->id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de l\'upload du fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified file.
*/
public function show(string $id): FileResource|JsonResponse
{
try {
$file = $this->fileRepository->find($id);
if (!$file) {
return response()->json([
'message' => 'Fichier non trouvé.',
], 404);
}
return new FileResource($file);
} catch (\Exception $e) {
Log::error('Error fetching file: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'file_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified file metadata.
*/
public function update(UpdateFileRequest $request, string $id): FileResource|JsonResponse
{
try {
$file = $this->fileRepository->find($id);
if (!$file) {
return response()->json([
'message' => 'Fichier non trouvé.',
], 404);
}
$validatedData = $request->validated();
// If category or client changed, move the file
if (isset($validatedData['category']) || isset($validatedData['client_id']) || isset($validatedData['subcategory'])) {
$newStoragePath = $this->generateOrganizedPath(
$validatedData['category'] ?? $this->extractCategoryFromPath($file->storage_uri),
$validatedData['client_id'] ?? $this->extractClientFromPath($file->storage_uri),
$validatedData['subcategory'] ?? $this->extractSubcategoryFromPath($file->storage_uri),
$file->file_name
);
if ($newStoragePath !== $file->storage_uri) {
// Move file to new location
Storage::disk('public')->move($file->storage_uri, $newStoragePath);
$validatedData['storage_uri'] = $newStoragePath;
}
}
$updated = $this->fileRepository->update($id, $validatedData);
if (!$updated) {
return response()->json([
'message' => 'Fichier non trouvé ou échec de la mise à jour.',
], 404);
}
$updatedFile = $this->fileRepository->find($id);
return new FileResource($updatedFile);
} catch (\Exception $e) {
Log::error('Error updating file: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'file_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified file.
*/
public function destroy(string $id): JsonResponse
{
try {
$file = $this->fileRepository->find($id);
if (!$file) {
return response()->json([
'message' => 'Fichier non trouvé.',
], 404);
}
// Delete file from storage
Storage::disk('public')->delete($file->storage_uri);
// Delete from database
$deleted = $this->fileRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Fichier non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Fichier supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting file: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'file_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du fichier.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get files by category.
*/
public function byCategory(Request $request, string $category): FileCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$files = $this->fileRepository->getByCategory($category, $perPage);
return new FileCollection($files);
} catch (\Exception $e) {
Log::error('Error fetching files by category: ' . $e->getMessage(), [
'exception' => $e,
'category' => $category,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers par catégorie.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get files by client.
*/
public function byClient(Request $request, int $clientId): FileCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$files = $this->fileRepository->getByClient($clientId, $perPage);
return new FileCollection($files);
} catch (\Exception $e) {
Log::error('Error fetching files by client: ' . $e->getMessage(), [
'exception' => $e,
'client_id' => $clientId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fichiers du client.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get organized file structure.
*/
public function organized(): JsonResponse
{
try {
$organizedFiles = $this->fileRepository->getOrganizedFiles();
return response()->json([
'data' => $organizedFiles,
'message' => 'Structure de fichiers récupérée avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error fetching organized files: ' . $e->getMessage(), [
'exception' => $e,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la structure de fichiers.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get storage statistics.
*/
public function stats(): JsonResponse
{
try {
$stats = $this->fileRepository->getStorageStats();
return response()->json([
'data' => $stats,
'message' => 'Statistiques de stockage récupérées avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error fetching storage stats: ' . $e->getMessage(), [
'exception' => $e,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des statistiques.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Download a file.
*/
public function download(string $id): JsonResponse
{
try {
$file = $this->fileRepository->find($id);
if (!$file) {
return response()->json([
'message' => 'Fichier non trouvé.',
], 404);
}
if (!Storage::disk('public')->exists($file->storage_uri)) {
return response()->json([
'message' => 'Fichier physique non trouvé sur le stockage.',
], 404);
}
$downloadUrl = Storage::disk('public')->url($file->storage_uri);
return response()->json([
'data' => [
'download_url' => $downloadUrl,
'file_name' => $file->file_name,
'mime_type' => $file->mime_type,
],
'message' => 'URL de téléchargement générée avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error generating download URL: ' . $e->getMessage(), [
'exception' => $e,
'file_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la génération de l\'URL de téléchargement.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Generate organized storage path.
*/
private function generateOrganizedPath(string $category, ?int $clientId, ?string $subcategory, string $fileName): string
{
$pathParts = [];
if ($clientId) {
$pathParts[] = 'client';
$pathParts[] = $clientId;
} else {
$pathParts[] = 'general';
}
$pathParts[] = $category;
if ($subcategory) {
$pathParts[] = Str::slug($subcategory);
} else {
$pathParts[] = 'files';
}
// Add timestamp to avoid conflicts
$timestamp = now()->format('Y-m-d_H-i-s');
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$basename = pathinfo($fileName, PATHINFO_FILENAME);
$safeFilename = Str::slug($basename) . '_' . $timestamp . '.' . $extension;
$pathParts[] = $safeFilename;
return implode('/', $pathParts);
}
/**
* Extract category from storage path.
*/
private function extractCategoryFromPath(string $storageUri): string
{
$pathParts = explode('/', $storageUri);
return $pathParts[count($pathParts) - 3] ?? 'general';
}
/**
* Extract client ID from storage path.
*/
private function extractClientFromPath(string $storageUri): ?int
{
$pathParts = explode('/', $storageUri);
if (count($pathParts) >= 4 && $pathParts[0] === 'client') {
return (int) $pathParts[1];
}
return null;
}
/**
* Extract subcategory from storage path.
*/
private function extractSubcategoryFromPath(string $storageUri): ?string
{
$pathParts = explode('/', $storageUri);
return $pathParts[count($pathParts) - 2] ?? null;
}
}

View File

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreFournisseurRequest;
use App\Http\Requests\UpdateFournisseurRequest;
use App\Http\Resources\Fournisseur\FournisseurResource;
use App\Http\Resources\Fournisseur\FournisseurCollection;
use App\Repositories\FournisseurRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class FournisseurController extends Controller
{
public function __construct(
private readonly FournisseurRepositoryInterface $fournisseurRepository
) {
}
/**
* Display a listing of fournisseurs.
*/
public function index(Request $request): FournisseurCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'is_active' => $request->get('is_active'),
'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 !== '';
});
$fournisseurs = $this->fournisseurRepository->paginate($perPage, $filters);
return new FournisseurCollection($fournisseurs);
} catch (\Exception $e) {
Log::error('Error fetching fournisseurs: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fournisseurs.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created fournisseur.
*/
public function store(StoreFournisseurRequest $request): FournisseurResource|JsonResponse
{
try {
$fournisseur = $this->fournisseurRepository->create($request->validated());
return new FournisseurResource($fournisseur);
} catch (\Exception $e) {
Log::error('Error creating fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified fournisseur.
*/
public function show(string $id): FournisseurResource|JsonResponse
{
try {
$fournisseur = $this->fournisseurRepository->find($id);
if (!$fournisseur) {
return response()->json([
'message' => 'Fournisseur non trouvé.',
], 404);
}
return new FournisseurResource($fournisseur);
} catch (\Exception $e) {
Log::error('Error fetching fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function searchBy(Request $request): JsonResponse
{
try {
$name = $request->get('name', '');
if (empty($name)) {
return response()->json([
'message' => 'Le paramètre "name" est requis.',
], 400);
}
$fournisseurs = $this->fournisseurRepository->searchByName($name);
return response()->json([
'data' => $fournisseurs,
'count' => $fournisseurs->count(),
'message' => $fournisseurs->count() > 0
? 'Fournisseurs trouvés avec succès.'
: 'Aucun fournisseur trouvé.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching fournisseurs by name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'search_term' => $name,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des fournisseurs.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified fournisseur.
*/
public function update(UpdateFournisseurRequest $request, string $id): FournisseurResource|JsonResponse
{
try {
$updated = $this->fournisseurRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Fournisseur non trouvé ou échec de la mise à jour.',
], 404);
}
$fournisseur = $this->fournisseurRepository->find($id);
return new FournisseurResource($fournisseur);
} catch (\Exception $e) {
Log::error('Error updating fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified fournisseur.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->fournisseurRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Fournisseur non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Fournisseur supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,557 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInterventionRequest;
use App\HttpRequests\StoreInterventionWithAllDataRequest;
use App\Http\Requests\UpdateInterventionRequest;
use App\Http\Resources\Intervention\InterventionResource;
use App\Http\Resources\Intervention\InterventionCollection;
use App\Repositories\InterventionRepositoryInterface;
use App\Repositories\InterventionPractitionerRepositoryInterface;
use App\Repositories\ClientRepositoryInterface;
use App\Repositories\ContactRepositoryInterface;
use App\Repositories\DeceasedRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class InterventionController extends Controller
{
/**
* @var InterventionRepositoryInterface
*/
protected $interventionRepository;
/**
* @var InterventionPractitionerRepositoryInterface
*/
protected $interventionPractitionerRepository;
/**
* @var ClientRepositoryInterface
*/
protected $clientRepository;
/**
* @var ContactRepositoryInterface
*/
protected $contactRepository;
/**
* @var DeceasedRepositoryInterface
*/
protected $deceasedRepository;
/**
* InterventionController constructor.
*
* @param InterventionRepositoryInterface $interventionRepository
* @param InterventionPractitionerRepositoryInterface $interventionPractitionerRepository
* @param ClientRepositoryInterface $clientRepository
* @param ContactRepositoryInterface $contactRepository
* @param DeceasedRepositoryInterface $deceasedRepository
*/
public function __construct(
InterventionRepositoryInterface $interventionRepository,
InterventionPractitionerRepositoryInterface $interventionPractitionerRepository,
ClientRepositoryInterface $clientRepository,
ContactRepositoryInterface $contactRepository,
DeceasedRepositoryInterface $deceasedRepository
) {
$this->interventionRepository = $interventionRepository;
$this->interventionPractitionerRepository = $interventionPractitionerRepository;
$this->clientRepository = $clientRepository;
$this->contactRepository = $contactRepository;
$this->deceasedRepository = $deceasedRepository;
}
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
try {
$filters = $request->only([
'client_id',
'deceased_id',
'status',
'type',
'start_date',
'end_date',
'sort_by',
'sort_order'
]);
$perPage = $request->input('per_page', 15);
$interventions = $this->interventionRepository->getAllPaginated($filters, $perPage);
return response()->json(new InterventionCollection($interventions));
} catch (\Exception $e) {
Log::error('Error fetching interventions list: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des interventions.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreInterventionRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$intervention = $this->interventionRepository->create($validated);
return response()->json(new InterventionResource($intervention), Response::HTTP_CREATED);
} catch (\Exception $e) {
Log::error('Error creating intervention: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'intervention.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Create an intervention with all related data (deceased, client, contact, location, documents).
*/
public function createInterventionalldata(StoreInterventionWithAllDataRequest $request): JsonResponse
{
try {
$validated = $request->validated();
// Wrap everything in a database transaction
$result = DB::transaction(function () use ($validated) {
// Step 1: Create the deceased
$deceasedData = $validated['deceased'];
$deceased = $this->deceasedRepository->create($deceasedData);
// Step 2: Create the client
$clientData = $validated['client'];
$client = $this->clientRepository->create($clientData);
// Step 3: Create the contact (if provided)
$contactId = null;
if (!empty($validated['contact'])) {
$contactData = array_merge($validated['contact'], [
'client_id' => $client->id
]);
$contact = $this->contactRepository->create($contactData);
$contactId = $contact->id;
}
// Step 4: Prepare location data (for now, we'll include it in intervention notes)
// In the future, you might want to create a ClientLocation entry
$locationData = $validated['location'] ?? [];
$locationNotes = '';
if (!empty($locationData)) {
$locationParts = [];
if (!empty($locationData['name'])) {
$locationParts[] = 'Lieu: ' . $locationData['name'];
}
if (!empty($locationData['address'])) {
$locationParts[] = 'Adresse: ' . $locationData['address'];
}
if (!empty($locationData['city'])) {
$locationParts[] = 'Ville: ' . $locationData['city'];
}
if (!empty($locationData['access_instructions'])) {
$locationParts[] = 'Instructions: ' . $locationData['access_instructions'];
}
if (!empty($locationData['notes'])) {
$locationParts[] = 'Notes: ' . $locationData['notes'];
}
$locationNotes = !empty($locationParts) ? "\n\n" . implode("\n", $locationParts) : '';
}
// Step 5: Create the intervention
$interventionData = array_merge($validated['intervention'], [
'deceased_id' => $deceased->id,
'client_id' => $client->id,
'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes
]);
$intervention = $this->interventionRepository->create($interventionData);
// Step 6: Handle document uploads (if any)
$documents = $validated['documents'] ?? [];
if (!empty($documents)) {
foreach ($documents as $documentData) {
if (isset($documentData['file']) && $documentData['file']->isValid()) {
// Store the file and create intervention attachment
// This is a placeholder - implement actual file upload logic
// $path = $documentData['file']->store('intervention_documents');
// Create intervention attachment record
}
}
}
// Return all created data
return [
'intervention' => $intervention,
'deceased' => $deceased,
'client' => $client,
'contact_id' => $contactId,
'documents_count' => count($documents)
];
});
Log::info('Intervention with all data created successfully', [
'intervention_id' => $result['intervention']->id,
'deceased_id' => $result['deceased']->id,
'client_id' => $result['client']->id,
'documents_count' => $result['documents_count']
]);
return response()->json([
'message' => 'Intervention créée avec succès',
'data' => [
'intervention' => new InterventionResource($result['intervention']),
'deceased' => $result['deceased'],
'client' => $result['client'],
'contact_id' => $result['contact_id'],
'documents_count' => $result['documents_count']
]
], Response::HTTP_CREATED);
} catch (\Illuminate\Validation\ValidationException $e) {
// Validation errors are handled by the FormRequest
return response()->json([
'message' => 'Données invalides',
'errors' => $e->errors()
], Response::HTTP_UNPROCESSABLE_ENTITY);
} catch (\Exception $e) {
Log::error('Error creating intervention with all data: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString(),
'input' => $request->except(['documents']) // Don't log file data
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'intervention.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Display the specified resource.
*/
public function show(int $id): JsonResponse
{
try {
$intervention = $this->interventionRepository->findById($id);
return response()->json(new InterventionResource($intervention));
} catch (\Exception $e) {
Log::error('Error fetching intervention details: ' . $e->getMessage());
return response()->json([
'message' => 'Intervention non trouvée ou une erreur est survenue.',
'error' => $e->getMessage()
], Response::HTTP_NOT_FOUND);
}
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateInterventionRequest $request, int $id): JsonResponse
{
try {
$intervention = $this->interventionRepository->findById($id);
$validated = $request->validated();
$updatedIntervention = $this->interventionRepository->update($intervention, $validated);
return response()->json(new InterventionResource($updatedIntervention));
} catch (\Exception $e) {
Log::error('Error updating intervention: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de l\'intervention.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy(int $id): JsonResponse
{
try {
$intervention = $this->interventionRepository->findById($id);
$this->interventionRepository->delete($intervention);
return response()->json(null, Response::HTTP_NO_CONTENT);
} catch (\Exception $e) {
Log::error('Error deleting intervention: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de l\'intervention.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Change the status of an intervention.
*/
public function changeStatus(Request $request, int $id): JsonResponse
{
try {
$validated = $request->validate([
'status' => 'required|in:demande,planifie,en_cours,termine,annule'
]);
$intervention = $this->interventionRepository->findById($id);
$updatedIntervention = $this->interventionRepository->changeStatus(
$intervention,
$validated['status']
);
return response()->json(new InterventionResource($updatedIntervention));
} catch (\Exception $e) {
Log::error('Error changing intervention status: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la modification du statut de l\'intervention.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Get interventions for a specific month.
*/
public function byMonth(Request $request): JsonResponse
{
try {
$validated = $request->validate([
'year' => 'required|integer|min:2000|max:2100',
'month' => 'required|integer|min:1|max:12'
]);
$interventions = $this->interventionRepository->getByMonth(
$validated['year'],
$validated['month']
);
return response()->json([
'data' => $interventions->map(function ($intervention) {
return new InterventionResource($intervention);
}),
'meta' => [
'total' => $interventions->count(),
'year' => $validated['year'],
'month' => $validated['month'],
]
]);
} catch (\Exception $e) {
Log::error('Error fetching interventions by month: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des interventions du mois.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Create assignment of practitioners to an intervention
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function createAssignment(Request $request, int $id): JsonResponse
{
try {
$validated = $request->validate([
'principal_practitioner_id' => 'nullable|integer|exists:thanatopractitioners,id',
'assistant_practitioner_ids' => 'nullable|array',
'assistant_practitioner_ids.*' => 'integer|exists:thanatopractitioners,id',
]);
$intervention = $this->interventionRepository->findById($id);
if (!$intervention) {
return response()->json([
'message' => 'Intervention non trouvée.'
], Response::HTTP_NOT_FOUND);
}
// Remove existing principal practitioner first
if (isset($validated['principal_practitioner_id'])) {
$principalId = $validated['principal_practitioner_id'];
$this->interventionPractitionerRepository->createAssignment($id, $principalId, 'principal');
}
// Handle assistant practitioners
if (isset($validated['assistant_practitioner_ids']) && is_array($validated['assistant_practitioner_ids'])) {
foreach ($validated['assistant_practitioner_ids'] as $assistantId) {
$this->interventionPractitionerRepository->createAssignment($id, $assistantId, 'assistant');
}
}
// Load the intervention with practitioners to return updated data
$intervention->load('practitioners');
$practitioners = $intervention->practitioners;
return response()->json([
'data' => new InterventionResource($intervention),
'message' => 'Assignment(s) créé(s) avec succès.',
'practitioners_count' => $practitioners->count(),
'practitioners' => $practitioners->map(function($p) {
return [
'id' => $p->id,
'full_name' => $p->full_name ?? ($p->first_name . ' ' . $p->last_name),
'role' => $p->pivot->role ?? 'unknown'
];
})->toArray()
], Response::HTTP_OK);
} catch (\Exception $e) {
return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'assignment.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Unassign a practitioner from an intervention
*
* @param Request $request
* @param int $interventionId
* @param int $practitionerId
* @return JsonResponse
*/
public function unassignPractitioner(Request $request, int $interventionId, int $practitionerId): JsonResponse
{
try {
Log::info('Unassigning practitioner from intervention', [
'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId
]);
// Validate that the intervention exists
$intervention = $this->interventionRepository->findById($interventionId);
if (!$intervention) {
return response()->json([
'message' => 'Intervention non trouvée.'
], Response::HTTP_NOT_FOUND);
}
// Check if the practitioner is actually assigned to this intervention
$isAssigned = $this->interventionPractitionerRepository->isPractitionerAssigned($interventionId, $practitionerId);
if (!$isAssigned) {
return response()->json([
'message' => 'Le praticien n\'est pas assigné à cette intervention.'
], Response::HTTP_NOT_FOUND);
}
// Remove the practitioner assignment
$deleted = $this->interventionPractitionerRepository->removeAssignment($interventionId, $practitionerId);
if ($deleted > 0) {
Log::info('Practitioner unassigned successfully', [
'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId,
'deleted_records' => $deleted
]);
// Reload intervention with remaining practitioners
$intervention->load('practitioners');
$remainingPractitioners = $intervention->practitioners;
return response()->json([
'data' => new InterventionResource($intervention),
'message' => 'Praticien désassigné avec succès.',
'remaining_practitioners_count' => $remainingPractitioners->count(),
'remaining_practitioners' => $remainingPractitioners->map(function($p) {
return [
'id' => $p->id,
'employee_name' => $p->employee->full_name ?? ($p->employee->first_name . ' ' . $p->employee->last_name),
'role' => $p->pivot->role ?? 'unknown'
];
})->toArray()
], Response::HTTP_OK);
} else {
Log::warning('No practitioner assignment found to delete', [
'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId
]);
return response()->json([
'message' => 'Aucun assignment de praticien trouvé à supprimer.'
], Response::HTTP_NOT_FOUND);
}
} catch (\Exception $e) {
Log::error('Error unassigning practitioner from intervention: ' . $e->getMessage(), [
'intervention_id' => $interventionId,
'practitioner_id' => $practitionerId,
'request_data' => $request->all(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la désassignation du praticien.',
'error' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Debug endpoint to check practitioners in database
*/
public function debugPractitioners(int $id): JsonResponse
{
try {
$intervention = $this->interventionRepository->findById($id);
// Direct database query
$dbPractitioners = DB::table('intervention_practitioner')
->where('intervention_id', $id)
->get();
// Eager loaded practitioners
$eagerPractitioners = $intervention->practitioners()->get();
return response()->json([
'intervention_id' => $id,
'database_records' => $dbPractitioners,
'eager_loaded_count' => $eagerPractitioners->count(),
'eager_loaded_data' => $eagerPractitioners->map(function($p) {
return [
'id' => $p->id,
'full_name' => $p->full_name ?? ($p->first_name . ' ' . $p->last_name),
'role' => $p->pivot->role ?? 'unknown'
];
})->toArray()
]);
} catch (\Exception $e) {
Log::error('Error in debug practitioners: ' . $e->getMessage());
return response()->json(['error' => $e->getMessage()], 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

@ -0,0 +1,349 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductCategoryRequest;
use App\Http\Requests\UpdateProductCategoryRequest;
use App\Http\Resources\ProductCategory\ProductCategoryResource;
use App\Http\Resources\ProductCategory\ProductCategoryCollection;
use App\Repositories\ProductCategoryRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class ProductCategoryController extends Controller
{
public function __construct(
private readonly ProductCategoryRepositoryInterface $productCategoryRepository
) {
}
/**
* Display a listing of product categories.
*/
public function index(Request $request): ProductCategoryCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'active' => $request->get('active'),
'parent_id' => $request->get('parent_id'),
'sort_by' => $request->get('sort_by', 'name'),
'sort_direction' => $request->get('sort_direction', 'asc'),
];
// Remove null filters
$filters = array_filter($filters, function ($value) {
return $value !== null && $value !== '';
});
$categories = $this->productCategoryRepository->paginate($perPage, $filters);
return new ProductCategoryCollection($categories);
} catch (\Exception $e) {
Log::error('Error fetching product categories: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des catégories de produits.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created product category.
*/
public function store(StoreProductCategoryRequest $request): ProductCategoryResource|JsonResponse
{
try {
$category = $this->productCategoryRepository->create($request->validated());
return new ProductCategoryResource($category);
} catch (\Exception $e) {
Log::error('Error creating product category: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création de la catégorie.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified product category.
*/
public function show(string $id): ProductCategoryResource|JsonResponse
{
try {
$category = $this->productCategoryRepository->find($id);
if (!$category) {
return response()->json([
'message' => 'Catégorie non trouvée.',
], 404);
}
return new ProductCategoryResource($category);
} catch (\Exception $e) {
Log::error('Error fetching product category: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'category_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la catégorie.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified product category.
*/
public function update(UpdateProductCategoryRequest $request, string $id): ProductCategoryResource|JsonResponse
{
try {
$updated = $this->productCategoryRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Catégorie non trouvée ou échec de la mise à jour.',
], 404);
}
$category = $this->productCategoryRepository->find($id);
return new ProductCategoryResource($category);
} catch (\Exception $e) {
Log::error('Error updating product category: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'category_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de la catégorie.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified product category.
*/
public function destroy(string $id): JsonResponse
{
try {
// Check if category can be deleted
if (!$this->productCategoryRepository->canDelete($id)) {
return response()->json([
'message' => 'Impossible de supprimer cette catégorie. Elle peut avoir des sous-catégories ou des produits associés.',
], 422);
}
$deleted = $this->productCategoryRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Catégorie non trouvée ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Catégorie supprimée avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting product category: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'category_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de la catégorie.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get active categories only
*/
public function active(): ProductCategoryCollection|JsonResponse
{
try {
$categories = $this->productCategoryRepository->getActive();
return new ProductCategoryCollection(collect(['data' => $categories]));
} catch (\Exception $e) {
Log::error('Error fetching active product categories: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des catégories actives.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get root categories (no parent)
*/
public function roots(): ProductCategoryCollection|JsonResponse
{
try {
$categories = $this->productCategoryRepository->getRoots();
return new ProductCategoryCollection(collect(['data' => $categories]));
} catch (\Exception $e) {
Log::error('Error fetching root product categories: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des catégories racine.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get categories with their children (hierarchical structure)
*/
public function hierarchical(): ProductCategoryCollection|JsonResponse
{
try {
$categories = $this->productCategoryRepository->getWithChildren();
return new ProductCategoryCollection(collect(['data' => $categories]));
} catch (\Exception $e) {
Log::error('Error fetching hierarchical product categories: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la structure hiérarchique.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Search categories by name, code or description
*/
public function search(Request $request): ProductCategoryCollection|JsonResponse
{
try {
$term = $request->get('term', '');
$perPage = $request->get('per_page', 15);
if (empty($term)) {
return response()->json([
'message' => 'Le paramètre "term" est requis pour la recherche.',
], 400);
}
$categories = $this->productCategoryRepository->search($term, $perPage);
return new ProductCategoryCollection($categories);
} catch (\Exception $e) {
Log::error('Error searching product categories: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'search_term' => $term,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des catégories.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get category statistics
*/
public function statistics(): JsonResponse
{
try {
$stats = $this->productCategoryRepository->getStatistics();
return response()->json([
'data' => $stats,
'message' => 'Statistiques des catégories récupérées avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error fetching product category 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);
}
}
/**
* Toggle category active status
*/
public function toggleActive(Request $request, string $id): ProductCategoryResource|JsonResponse
{
try {
$request->validate([
'active' => 'required|boolean',
]);
$category = $this->productCategoryRepository->find($id);
if (!$category) {
return response()->json([
'message' => 'Catégorie non trouvée.',
], 404);
}
$updated = $this->productCategoryRepository->update($id, [
'active' => $request->boolean('active')
]);
if (!$updated) {
return response()->json([
'message' => 'Échec de la mise à jour du statut.',
], 422);
}
$category = $this->productCategoryRepository->find($id);
return new ProductCategoryResource($category);
} catch (\Exception $e) {
Log::error('Error toggling product category status: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'category_id' => $id,
'data' => $request->all(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du statut.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,375 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\Product\ProductResource;
use App\Http\Resources\Product\ProductCollection;
use App\Repositories\ProductRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository
) {
}
/**
* Display a listing of products.
*/
public function index(Request $request): ProductCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'categorie' => $request->get('categorie_id'),
'fournisseur_id' => $request->get('fournisseur_id'),
'low_stock' => $request->get('low_stock'),
'expiring_soon' => $request->get('expiring_soon'),
'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 !== '';
});
$products = $this->productRepository->paginate($perPage, $filters);
return new ProductCollection($products);
} catch (\Exception $e) {
Log::error('Error fetching products: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des produits.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created product.
*/
public function store(StoreProductRequest $request): ProductResource|JsonResponse
{
try {
$validatedData = $request->validated();
// Handle image upload
if ($request->hasFile('image')) {
// Create product without image first
$product = $this->productRepository->create($validatedData);
// Upload and attach image
$imagePath = $product->uploadImage($request->file('image'));
// Refresh product to get updated data
$product = $this->productRepository->find($product->id);
} else {
$product = $this->productRepository->create($validatedData);
}
return new ProductResource($product);
} catch (\Exception $e) {
Log::error('Error creating product: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du produit.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified product.
*/
public function show(string $id): ProductResource|JsonResponse
{
try {
$product = $this->productRepository->find($id);
if (!$product) {
return response()->json([
'message' => 'Produit non trouvé.',
], 404);
}
return new ProductResource($product);
} catch (\Exception $e) {
Log::error('Error fetching product: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'product_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du produit.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Search products by name.
*/
public function searchBy(Request $request): JsonResponse
{
try {
$name = $request->get('name', '');
$exact = $request->boolean('exact', false);
if (empty($name)) {
return response()->json([
'message' => 'Le paramètre "name" est requis.',
], 400);
}
$products = $this->productRepository->searchByName($name, 15, $exact);
return response()->json([
'data' => $products,
'count' => $products->count(),
'message' => $products->count() > 0
? 'Produits trouvés avec succès.'
: 'Aucun produit trouvé.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching products by name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'search_term' => $name,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des produits.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get products with low stock.
*/
public function lowStock(Request $request): ProductCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$products = $this->productRepository->getLowStockProducts($perPage);
return new ProductCollection($products);
} catch (\Exception $e) {
Log::error('Error fetching low stock products: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des produits à stock faible.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get products by category.
*/
public function byCategory(Request $request): ProductCollection|JsonResponse
{
try {
$categoryId = $request->get('category_id');
$perPage = $request->get('per_page', 15);
if (empty($categoryId)) {
return response()->json([
'message' => 'Le paramètre "category_id" est requis.',
], 400);
}
$products = $this->productRepository->getByCategory($categoryId, $perPage);
return new ProductCollection($products);
} catch (\Exception $e) {
Log::error('Error fetching products by category: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'category_id' => $categoryId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des produits par catégorie.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get products statistics.
*/
public function statistics(): JsonResponse
{
try {
$stats = $this->productRepository->getStatistics();
return response()->json([
'data' => $stats,
'message' => 'Statistiques des produits récupérées avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error fetching product 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);
}
}
/**
* Update the specified product.
*/
public function update(UpdateProductRequest $request, string $id): ProductResource|JsonResponse
{
try {
$validatedData = $request->validated();
$product = $this->productRepository->find($id);
if (!$product) {
return response()->json([
'message' => 'Produit non trouvé.',
], 404);
}
// Handle image upload/removal
if ($request->boolean('remove_image')) {
// Remove existing image
$product->deleteImage();
} elseif ($request->hasFile('image')) {
// Upload new image
$product->uploadImage($request->file('image'));
}
// Remove image-related fields from validated data before updating other fields
unset($validatedData['image'], $validatedData['remove_image']);
// Update other product fields
$updated = $this->productRepository->update($id, $validatedData);
if (!$updated) {
return response()->json([
'message' => 'Produit non trouvé ou échec de la mise à jour.',
], 404);
}
$product = $this->productRepository->find($id);
return new ProductResource($product);
} catch (\Exception $e) {
Log::error('Error updating product: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'product_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du produit.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified product.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->productRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Produit non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Produit supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting product: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'product_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du produit.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update stock quantity for a product.
*/
public function updateStock(Request $request, string $id): JsonResponse
{
try {
$request->validate([
'stock_actuel' => 'required|numeric|min:0',
]);
$updated = $this->productRepository->updateStock((int) $id, $request->stock_actuel);
if (!$updated) {
return response()->json([
'message' => 'Produit non trouvé ou échec de la mise à jour du stock.',
], 404);
}
$product = $this->productRepository->find($id);
return response()->json([
'data' => new ProductResource($product),
'message' => 'Stock mis à jour avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error updating product stock: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'product_id' => $id,
'stock_data' => $request->all(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,357 @@
<?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->findById((int) $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);
}
}
/**
* Search thanatopractitioners by employee name.
*/
public function searchByEmployeeName(Request $request): JsonResponse
{
try {
$query = $request->get('query', '');
if (strlen($query) < 2) {
return response()->json([
'data' => [],
'message' => 'Veuillez entrer au moins 2 caractères pour la recherche.',
], 200);
}
$thanatopractitioners = $this->thanatopractitionerRepository->searchByEmployeeName($query);
return response()->json([
'data' => new ThanatopractitionerCollection($thanatopractitioners),
'message' => 'Recherche effectuée avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching thanatopractitioners by employee name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'query' => $request->get('query'),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche.',
'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,70 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ClientCategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$categoryId = $this->route('client_category')?->id;
return [
'name' => [
'required',
'string',
'max:255',
Rule::unique('client_categories')->ignore($categoryId)
],
'slug' => [
'nullable',
'string',
'max:255',
'alpha_dash',
Rule::unique('client_categories')->ignore($categoryId)
],
'description' => 'nullable|string|max:1000',
'is_active' => 'boolean',
'sort_order' => 'nullable|integer|min:0',
];
}
public function attributes(): array
{
return [
'name' => 'category name',
'slug' => 'URL slug',
];
}
protected function prepareForValidation(): void
{
// Generate slug from name if not provided and name exists
if (!$this->slug && $this->name) {
$this->merge([
'slug' => \Str::slug($this->name),
]);
}
// Ensure boolean values are properly cast
if ($this->has('is_active')) {
$this->merge([
'is_active' => filter_var($this->is_active, FILTER_VALIDATE_BOOLEAN),
]);
}
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientGroupRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:191|unique:client_groups,name',
'description' => 'nullable|string',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du groupe est obligatoire.',
'name.string' => 'Le nom du groupe doit être une chaîne de caractères.',
'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.',
'name.unique' => 'Un groupe avec ce nom existe déjà.',
'description.string' => 'La description doit être une chaîne de caractères.',
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientLocationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => 'required|exists:clients,id',
'name' => 'nullable|string|max:191',
'address_line1' => 'nullable|string|max:255',
'address_line2' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:20',
'city' => 'nullable|string|max:191',
'country_code' => 'nullable|string|size:2',
'gps_lat' => 'nullable|numeric|between:-90,90',
'gps_lng' => 'nullable|numeric|between:-180,180',
'is_default' => 'boolean',
];
}
public function messages(): array
{
return [
'client_id.required' => 'Le client est obligatoire.',
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'country_code.size' => 'Le code pays doit contenir 2 caractères.',
'gps_lat.numeric' => 'La latitude doit être un nombre.',
'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.',
'gps_lng.numeric' => 'La longitude doit être un nombre.',
'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.',
'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
if (empty($this->address_line1) && empty($this->postal_code) && empty($this->city)) {
$validator->errors()->add(
'general',
'Au moins un champ d\'adresse (adresse, code postal ou ville) doit être renseigné.'
);
}
});
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_category_id' => 'nullable',
'name' => 'required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'group_id' => 'nullable|exists:client_groups,id',
'notes' => 'nullable|string',
'is_active' => 'boolean',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
public function messages(): array
{
return [
'company_id.required' => 'La société est obligatoire.',
'company_id.exists' => 'La société sélectionnée n\'existe pas.',
'type.required' => 'Le type de client est obligatoire.',
'type.in' => 'Le type de client sélectionné est invalide.',
'name.required' => 'Le nom du client est obligatoire.',
'name.string' => 'Le nom du client doit être une chaîne de caractères.',
'name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.',
];
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreContactRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => 'nullable|exists:clients,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'first_name' => 'nullable|string|max:191',
'last_name' => 'nullable|string|max:191',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'role' => 'nullable|string|max:191',
];
}
public function messages(): array
{
return [
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
'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 191 caractères.',
'last_name.string' => 'Le nom doit être une chaîne de caractères.',
'last_name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'role.max' => 'Le rôle ne peut pas dépasser 191 caractères.',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
// At least one of client_id or fournisseur_id must be provided
if (empty($this->client_id) && empty($this->fournisseur_id)) {
$validator->errors()->add(
'general',
'Le contact doit être associé à un client ou un fournisseur.'
);
}
if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) {
$validator->errors()->add(
'general',
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
);
}
});
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreDeceasedDocumentRequest 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 [
'deceased_id' => 'required|exists:deceased,id',
'doc_type' => 'required|string|max:191',
'file_id' => 'nullable|exists:files,id',
'generated_at' => 'nullable|date',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'deceased_id.required' => 'Le défunt est obligatoire.',
'deceased_id.exists' => 'Le défunt 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.',
'generated_at.date' => 'La date de génération doit être une date valide.',
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreDeceasedRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Add authorization logic if needed
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'last_name' => ['required', 'string', 'max:191'],
'first_name' => ['nullable', 'string', 'max:191'],
'birth_date' => ['nullable', 'date'],
'death_date' => ['nullable', 'date', 'after_or_equal:birth_date'],
'place_of_death' => ['nullable', 'string', 'max:255'],
'notes' => ['nullable', 'string']
];
}
/**
* Get custom error messages for validator errors.
*/
public function messages(): array
{
return [
'last_name.required' => 'Le nom de famille est obligatoire.',
'last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.',
'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
'death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.',
'place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.'
];
}
}

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,109 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class StoreFileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Auth::check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'file' => 'required|file|max:10240', // Max 10MB
'file_name' => 'nullable|string|max:255',
'category' => 'required|string|in:devis,facture,contrat,document,image,autre',
'client_id' => 'nullable|integer|exists:clients,id',
'subcategory' => 'nullable|string|max:100',
'description' => 'nullable|string|max:500',
'tags' => 'nullable|array|max:10',
'tags.*' => 'string|max:50',
'is_public' => 'boolean',
];
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [
'file' => 'fichier',
'file_name' => 'nom du fichier',
'category' => 'catégorie',
'client_id' => 'client',
'subcategory' => 'sous-catégorie',
'description' => 'description',
'tags' => 'étiquettes',
'is_public' => 'visibilité publique',
];
}
/**
* Get the error messages for the defined validation rules.
*/
public function messages(): array
{
return [
'file.required' => 'Le fichier est obligatoire.',
'file.file' => 'Le fichier doit être un fichier valide.',
'file.max' => 'Le fichier ne peut pas dépasser 10 MB.',
'file_name.string' => 'Le nom du fichier doit être une chaîne de caractères.',
'file_name.max' => 'Le nom du fichier ne peut pas dépasser 255 caractères.',
'category.required' => 'La catégorie est obligatoire.',
'category.in' => 'La catégorie sélectionnée n\'est pas valide.',
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'subcategory.string' => 'La sous-catégorie doit être une chaîne de caractères.',
'subcategory.max' => 'La sous-catégorie ne peut pas dépasser 100 caractères.',
'description.string' => 'La description doit être une chaîne de caractères.',
'description.max' => 'La description ne peut pas dépasser 500 caractères.',
'tags.array' => 'Les étiquettes doivent être un tableau.',
'tags.max' => 'Vous ne pouvez pas ajouter plus de 10 étiquettes.',
'tags.*.string' => 'Chaque étiquette doit être une chaîne de caractères.',
'tags.*.max' => 'Chaque étiquette ne peut pas dépasser 50 caractères.',
'is_public.boolean' => 'La visibilité publique doit être vrai ou faux.',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Set default values
$this->merge([
'uploaded_by' => $this->user()->id,
'is_public' => $this->boolean('is_public', false),
'category' => $this->input('category', 'autre'), // Default category to 'autre' if not provided
]);
// If no file_name provided, use the original file name
if (!$this->has('file_name') && $this->hasFile('file')) {
$this->merge([
'file_name' => $this->file->getClientOriginalName(),
]);
}
// Extract file information
if ($this->hasFile('file')) {
$file = $this->file;
$this->merge([
'mime_type' => $file->getMimeType(),
'size_bytes' => $file->getSize(),
]);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreFournisseurRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'notes' => 'nullable|string',
'is_active' => 'boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du fournisseur est obligatoire.',
'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.',
'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreInterventionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Add authorization logic if needed
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => ['required', 'exists:clients,id'],
'deceased_id' => ['nullable', 'exists:deceased,id'],
'order_giver' => ['nullable', 'string', 'max:255'],
'location_id' => ['nullable', 'exists:client_locations,id'],
'type' => ['required', Rule::in([
'thanatopraxie',
'toilette_mortuaire',
'exhumation',
'retrait_pacemaker',
'retrait_bijoux',
'autre'
])],
'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
'duration_min' => ['nullable', 'integer', 'min:0'],
'status' => ['sometimes', Rule::in([
'demande',
'planifie',
'en_cours',
'termine',
'annule'
])],
'practitioners' => ['nullable', 'array'],
'practitioners.*' => ['exists:thanatopractitioners,id'],
'principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
'assistant_practitioner_ids' => ['nullable', 'array'],
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
'notes' => ['nullable', 'string'],
'created_by' => ['nullable', 'exists:users,id']
];
}
/**
* Get custom error messages for validator errors.
*/
public function messages(): array
{
return [
'client_id.required' => 'Le client est obligatoire.',
'client_id.exists' => 'Le client sélectionné est invalide.',
'deceased_id.exists' => 'Le défunt sélectionné est invalide.',
'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
'location_id.exists' => 'Le lieu sélectionné est invalide.',
'type.required' => 'Le type d\'intervention est obligatoire.',
'type.in' => 'Le type d\'intervention est invalide.',
'scheduled_at.date_format' => 'Le format de la date programmée est invalide.',
'duration_min.integer' => 'La durée doit être un nombre entier.',
'duration_min.min' => 'La durée ne peut pas être négative.',
'status.in' => 'Le statut de l\'intervention est invalide.',
'practitioners.array' => 'Les praticiens doivent être un tableau.',
'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
'created_by.exists' => 'L\'utilisateur créateur est invalide.'
];
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreInterventionWithAllDataRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'deceased' => 'required|array',
'deceased.last_name' => ['required', 'string', 'max:191'],
'deceased.first_name' => ['nullable', 'string', 'max:191'],
'deceased.birth_date' => ['nullable', 'date'],
'deceased.death_date' => ['nullable', 'date', 'after_or_equal:deceased.birth_date'],
'deceased.place_of_death' => ['nullable', 'string', 'max:255'],
'deceased.notes' => ['nullable', 'string'],
'client' => 'required|array',
'client.name' => ['required', 'string', 'max:255'],
'client.vat_number' => ['nullable', 'string', 'max:32'],
'client.siret' => ['nullable', 'string', 'max:20'],
'client.email' => ['nullable', 'email', 'max:191'],
'client.phone' => ['nullable', 'string', 'max:50'],
'client.billing_address_line1' => ['nullable', 'string', 'max:255'],
'client.billing_address_line2' => ['nullable', 'string', 'max:255'],
'client.billing_postal_code' => ['nullable', 'string', 'max:20'],
'client.billing_city' => ['nullable', 'string', 'max:191'],
'client.billing_country_code' => ['nullable', 'string', 'size:2'],
'client.notes' => ['nullable', 'string'],
'contact' => 'nullable|array',
'contact.first_name' => ['nullable', 'string', 'max:191'],
'contact.last_name' => ['nullable', 'string', 'max:191'],
'contact.email' => ['nullable', 'email', 'max:191'],
'contact.phone' => ['nullable', 'string', 'max:50'],
'contact.role' => ['nullable', 'string', 'max:191'],
'location' => 'nullable|array',
'location.name' => ['nullable', 'string', 'max:255'],
'location.address' => ['nullable', 'string', 'max:255'],
'location.city' => ['nullable', 'string', 'max:191'],
'location.postal_code' => ['nullable', 'string', 'max:20'],
'location.country_code' => ['nullable', 'string', 'size:2'],
'location.access_instructions' => ['nullable', 'string'],
'location.notes' => ['nullable', 'string'],
'documents' => 'nullable|array',
'documents.*.file' => ['required', 'file'],
'documents.*.name' => ['required', 'string', 'max:255'],
'documents.*.description' => ['nullable', 'string'],
'intervention' => 'required|array',
'intervention.type' => ['required', Rule::in([
'thanatopraxie',
'toilette_mortuaire',
'exhumation',
'retrait_pacemaker',
'retrait_bijoux',
'autre'
])],
'intervention.scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
'intervention.duration_min' => ['nullable', 'integer', 'min:0'],
'intervention.status' => ['sometimes', Rule::in([
'demande',
'planifie',
'en_cours',
'termine',
'annule'
])],
'intervention.practitioners' => ['nullable', 'array'],
'intervention.practitioners.*' => ['exists:thanatopractitioners,id'],
'intervention.principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
'intervention.assistant_practitioner_ids' => ['nullable', 'array'],
'intervention.assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
'intervention.order_giver' => ['nullable', 'string', 'max:255'],
'intervention.notes' => ['nullable', 'string'],
'intervention.created_by' => ['nullable', 'exists:users,id']
];
}
/**
* Get custom error messages for validator errors.
*/
public function messages(): array
{
$messages = [
'deceased.required' => 'Les informations du défunt sont obligatoires.',
'deceased.last_name.required' => 'Le nom de famille du défunt est obligatoire.',
'deceased.last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.',
'deceased.first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
'deceased.death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.',
'deceased.place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.',
'client.required' => 'Les informations du client sont obligatoires.',
'client.name.required' => 'Le nom du client est obligatoire.',
'client.name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.',
'client.vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'client.siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'client.email.email' => 'L\'adresse email doit être valide.',
'client.email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'client.phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'client.billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'client.billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'client.billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'client.billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'client.is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
'contact.first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
'contact.last_name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'contact.email.email' => 'L\'adresse email doit être valide.',
'contact.email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'contact.phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'contact.role.max' => 'Le rôle ne peut pas dépasser 191 caractères.',
'intervention.required' => 'Les informations de l\'intervention sont obligatoires.',
'intervention.type.required' => 'Le type d\'intervention est obligatoire.',
'intervention.type.in' => 'Le type d\'intervention est invalide.',
'intervention.scheduled_at.date_format' => 'Le format de la date programmée est invalide.',
'intervention.duration_min.integer' => 'La durée doit être un nombre entier.',
'intervention.duration_min.min' => 'La durée ne peut pas être négative.',
'intervention.status.in' => 'Le statut de l\'intervention est invalide.',
'intervention.practitioners.array' => 'Les praticiens doivent être un tableau.',
'intervention.practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
'intervention.principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
'intervention.assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
'intervention.assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
'intervention.order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
'intervention.created_by.exists' => 'L\'utilisateur créateur est invalide.'
];
// Add document-specific messages
$messages = array_merge($messages, [
'documents.array' => 'Les documents doivent être un tableau.',
'documents.*.file.required' => 'Chaque document doit avoir un fichier.',
'documents.*.name.required' => 'Le nom du document est obligatoire.',
'documents.*.name.max' => 'Le nom du document ne peut pas dépasser 255 caractères.',
]);
return $messages;
}
/**
* Handle a failed validation attempt.
*/
protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator)
{
$errors = $validator->errors();
// Group errors by step for better UX
$groupedErrors = [
'deceased' => [],
'client' => [],
'contact' => [],
'location' => [],
'documents' => [],
'intervention' => [],
'global' => []
];
foreach ($errors->messages() as $field => $messages) {
if (str_starts_with($field, 'deceased.')) {
$groupedErrors['deceased'] = array_merge($groupedErrors['deceased'], $messages);
} elseif (str_starts_with($field, 'client.')) {
$groupedErrors['client'] = array_merge($groupedErrors['client'], $messages);
} elseif (str_starts_with($field, 'contact.')) {
$groupedErrors['contact'] = array_merge($groupedErrors['contact'], $messages);
} elseif (str_starts_with($field, 'location.')) {
$groupedErrors['location'] = array_merge($groupedErrors['location'], $messages);
} elseif (str_starts_with($field, 'documents.')) {
$groupedErrors['documents'] = array_merge($groupedErrors['documents'], $messages);
} elseif (str_starts_with($field, 'intervention.')) {
$groupedErrors['intervention'] = array_merge($groupedErrors['intervention'], $messages);
} else {
$groupedErrors['global'] = array_merge($groupedErrors['global'], $messages);
}
}
parent::failedValidation($validator);
}
}

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

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductCategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'parent_id' => 'nullable|exists:product_categories,id',
'code' => 'required|string|max:64|unique:product_categories,code',
'name' => 'required|string|max:191',
'description' => 'nullable|string',
'active' => 'boolean',
];
}
public function messages(): array
{
return [
'parent_id.exists' => 'La catégorie parente sélectionnée n\'existe pas.',
'code.required' => 'Le code de la catégorie est obligatoire.',
'code.string' => 'Le code de la catégorie doit être une chaîne de caractères.',
'code.max' => 'Le code de la catégorie ne peut pas dépasser 64 caractères.',
'code.unique' => 'Ce code de catégorie existe déjà.',
'name.required' => 'Le nom de la catégorie est obligatoire.',
'name.string' => 'Le nom de la catégorie doit être une chaîne de caractères.',
'name.max' => 'Le nom de la catégorie ne peut pas dépasser 191 caractères.',
'description.string' => 'La description doit être une chaîne de caractères.',
'active.boolean' => 'Le statut actif doit être un booléen.',
];
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'nom' => 'required|string|max:255',
'reference' => 'required|string|max:100|unique:products,reference',
'categorie_id' => 'required|exists:product_categories,id',
'fabricant' => 'nullable|string|max:191',
'stock_actuel' => 'required|numeric|min:0',
'stock_minimum' => 'required|numeric|min:0',
'unite' => 'required|string|max:50',
'prix_unitaire' => 'required|numeric|min:0',
'date_expiration' => 'nullable|date|after:today',
'numero_lot' => 'nullable|string|max:100',
'conditionnement_nom' => 'nullable|string|max:191',
'conditionnement_quantite' => 'nullable|numeric|min:0',
'conditionnement_unite' => 'nullable|string|max:50',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
'remove_image' => 'nullable|boolean',
'fiche_technique_url' => 'nullable|url|max:500',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
];
}
public function messages(): array
{
return [
'nom.required' => 'Le nom du produit est obligatoire.',
'nom.string' => 'Le nom du produit doit être une chaîne de caractères.',
'nom.max' => 'Le nom du produit ne peut pas dépasser 255 caractères.',
'reference.required' => 'La référence du produit est obligatoire.',
'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_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.',
'stock_actuel.numeric' => 'Le stock actuel doit être un nombre.',
'stock_actuel.min' => 'Le stock actuel doit être supérieur ou égal à 0.',
'stock_minimum.required' => 'Le stock minimum est obligatoire.',
'stock_minimum.numeric' => 'Le stock minimum doit être un nombre.',
'stock_minimum.min' => 'Le stock minimum doit être supérieur ou égal à 0.',
'unite.required' => 'L\'unité est obligatoire.',
'unite.string' => 'L\'unité doit être une chaîne de caractères.',
'unite.max' => 'L\'unité ne peut pas dépasser 50 caractères.',
'prix_unitaire.required' => 'Le prix unitaire est obligatoire.',
'prix_unitaire.numeric' => 'Le prix unitaire doit être un nombre.',
'prix_unitaire.min' => 'Le prix unitaire doit être supérieur ou égal à 0.',
'date_expiration.date' => 'La date d\'expiration doit être une date valide.',
'date_expiration.after' => 'La date d\'expiration doit être postérieure à aujourd\'hui.',
'numero_lot.string' => 'Le numéro de lot doit être une chaîne de caractères.',
'numero_lot.max' => 'Le numéro de lot ne peut pas dépasser 100 caractères.',
'conditionnement_nom.string' => 'Le nom du conditionnement doit être une chaîne de caractères.',
'conditionnement_nom.max' => 'Le nom du conditionnement ne peut pas dépasser 191 caractères.',
'conditionnement_quantite.numeric' => 'La quantité du conditionnement doit être un nombre.',
'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.',
'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.',
'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.',
'image.image' => 'Le fichier doit être une image valide.',
'image.mimes' => 'L\'image doit être de type: jpeg, png, jpg, gif ou svg.',
'image.max' => 'L\'image ne peut pas dépasser 2MB.',
'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.',
'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.',
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
];
}
}

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,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateClientGroupRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$clientGroupId = $this->route('client_group') ? $this->route('client_group')->id : $this->route('id');
return [
'name' => [
'required',
'string',
'max:191',
Rule::unique('client_groups', 'name')->ignore($clientGroupId)
],
'description' => 'nullable|string',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du groupe est obligatoire.',
'name.string' => 'Le nom du groupe doit être une chaîne de caractères.',
'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.',
'name.unique' => 'Un groupe avec ce nom existe déjà.',
'description.string' => 'La description doit être une chaîne de caractères.',
];
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateClientLocationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => 'required|exists:clients,id',
'name' => 'nullable|string|max:191',
'address_line1' => 'nullable|string|max:255',
'address_line2' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:20',
'city' => 'nullable|string|max:191',
'country_code' => 'nullable|string|size:2',
'gps_lat' => 'nullable|numeric|between:-90,90',
'gps_lng' => 'nullable|numeric|between:-180,180',
'is_default' => 'boolean',
];
}
public function messages(): array
{
return [
'client_id.required' => 'Le client est obligatoire.',
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'country_code.size' => 'Le code pays doit contenir 2 caractères.',
'gps_lat.numeric' => 'La latitude doit être un nombre.',
'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.',
'gps_lng.numeric' => 'La longitude doit être un nombre.',
'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.',
'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateClientRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'group_id' => 'nullable|exists:client_groups,id',
'notes' => 'nullable|string',
'is_active' => 'boolean',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
public function messages(): array
{
return [
'company_id.required' => 'La société est obligatoire.',
'company_id.exists' => 'La société sélectionnée n\'existe pas.',
'type.required' => 'Le type de client est obligatoire.',
'type.in' => 'Le type de client sélectionné est invalide.',
'name.required' => 'Le nom du client est obligatoire.',
'name.string' => 'Le nom du client doit être une chaîne de caractères.',
'name.max' => 'Le nom du client ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.',
];
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateContactRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => 'nullable|exists:clients,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'first_name' => 'nullable|string|max:191',
'last_name' => 'nullable|string|max:191',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'role' => 'nullable|string|max:191',
];
}
public function messages(): array
{
return [
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
'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 191 caractères.',
'last_name.string' => 'Le nom doit être une chaîne de caractères.',
'last_name.max' => 'Le nom ne peut pas dépasser 191 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'role.max' => 'Le rôle ne peut pas dépasser 191 caractères.',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
// At least one of client_id or fournisseur_id must be provided
if (empty($this->client_id) && empty($this->fournisseur_id)) {
$validator->errors()->add(
'general',
'Le contact doit être associé à un client ou un fournisseur.'
);
}
if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) {
$validator->errors()->add(
'general',
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
);
}
});
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateDeceasedDocumentRequest 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 [
'deceased_id' => 'sometimes|required|exists:deceased,id',
'doc_type' => 'sometimes|required|string|max:191',
'file_id' => 'nullable|exists:files,id',
'generated_at' => 'nullable|date',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'deceased_id.required' => 'Le défunt est obligatoire.',
'deceased_id.exists' => 'Le défunt 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.',
'generated_at.date' => 'La date de génération doit être une date valide.',
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateDeceasedRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Add authorization logic if needed
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'last_name' => ['sometimes', 'required', 'string', 'max:191'],
'first_name' => ['nullable', 'string', 'max:191'],
'birth_date' => ['nullable', 'date'],
'death_date' => ['nullable', 'date', 'after_or_equal:birth_date'],
'place_of_death' => ['nullable', 'string', 'max:255'],
'notes' => ['nullable', 'string']
];
}
/**
* Get custom error messages for validator errors.
*/
public function messages(): array
{
return [
'last_name.required' => 'Le nom de famille est obligatoire.',
'last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.',
'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
'death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.',
'place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 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,96 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class UpdateFileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$file = $this->route('file');
// Allow if user owns the file or is admin
return Auth::check() && (
$file->uploaded_by === Auth::id() ||
Auth::user()->hasRole('admin')
);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'file_name' => 'sometimes|string|max:255',
'description' => 'nullable|string|max:500',
'tags' => 'nullable|array|max:10',
'tags.*' => 'string|max:50',
'is_public' => 'boolean',
'category' => 'sometimes|string|in:devis,facture,contrat,document,image,autre',
'client_id' => 'nullable|integer|exists:clients,id',
'subcategory' => 'nullable|string|max:100',
];
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [
'file_name' => 'nom du fichier',
'description' => 'description',
'tags' => 'étiquettes',
'is_public' => 'visibilité publique',
'category' => 'catégorie',
'client_id' => 'client',
'subcategory' => 'sous-catégorie',
];
}
/**
* Get the error messages for the defined validation rules.
*/
public function messages(): array
{
return [
'file_name.string' => 'Le nom du fichier doit être une chaîne de caractères.',
'file_name.max' => 'Le nom du fichier ne peut pas dépasser 255 caractères.',
'description.string' => 'La description doit être une chaîne de caractères.',
'description.max' => 'La description ne peut pas dépasser 500 caractères.',
'tags.array' => 'Les étiquettes doivent être un tableau.',
'tags.max' => 'Vous ne pouvez pas ajouter plus de 10 étiquettes.',
'tags.*.string' => 'Chaque étiquette doit être une chaîne de caractères.',
'tags.*.max' => 'Chaque étiquette ne peut pas dépasser 50 caractères.',
'is_public.boolean' => 'La visibilité publique doit être vrai ou faux.',
'category.string' => 'La catégorie doit être une chaîne de caractères.',
'category.in' => 'La catégorie sélectionnée n\'est pas valide.',
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
'subcategory.string' => 'La sous-catégorie doit être une chaîne de caractères.',
'subcategory.max' => 'La sous-catégorie ne peut pas dépasser 100 caractères.',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Only merge fields that are present in the request
$data = [];
if ($this->has('is_public')) {
$data['is_public'] = $this->boolean('is_public');
}
$this->merge($data);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateFournisseurRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'sometimes|required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'notes' => 'nullable|string',
'is_active' => 'boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du fournisseur est obligatoire.',
'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.',
'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateInterventionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Add authorization logic if needed
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'client_id' => ['sometimes', 'required', 'exists:clients,id'],
'deceased_id' => ['nullable', 'exists:deceased,id'],
'order_giver' => ['nullable', 'string', 'max:255'],
'location_id' => ['nullable', 'exists:client_locations,id'],
'type' => ['sometimes', 'required', Rule::in([
'thanatopraxie',
'toilette_mortuaire',
'exhumation',
'retrait_pacemaker',
'retrait_bijoux',
'autre'
])],
'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'],
'duration_min' => ['nullable', 'integer', 'min:0'],
'status' => ['sometimes', Rule::in([
'demande',
'planifie',
'en_cours',
'termine',
'annule'
])],
'practitioners' => ['nullable', 'array'],
'practitioners.*' => ['exists:thanatopractitioners,id'],
'principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
'assistant_practitioner_ids' => ['nullable', 'array'],
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
'notes' => ['nullable', 'string'],
'created_by' => ['nullable', 'exists:users,id']
];
}
/**
* Get custom error messages for validator errors.
*/
public function messages(): array
{
return [
'client_id.required' => 'Le client est obligatoire.',
'client_id.exists' => 'Le client sélectionné est invalide.',
'deceased_id.exists' => 'Le défunt sélectionné est invalide.',
'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
'location_id.exists' => 'Le lieu sélectionné est invalide.',
'type.required' => 'Le type d\'intervention est obligatoire.',
'type.in' => 'Le type d\'intervention est invalide.',
'scheduled_at.date_format' => 'Le format de la date programmée est invalide.',
'duration_min.integer' => 'La durée doit être un nombre entier.',
'duration_min.min' => 'La durée ne peut pas être négative.',
'status.in' => 'Le statut de l\'intervention est invalide.',
'practitioners.array' => 'Les praticiens doivent être un tableau.',
'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
'created_by.exists' => 'L\'utilisateur créateur est invalide.'
];
}
}

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

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProductCategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$categoryId = $this->route('id');
return [
'parent_id' => 'nullable|exists:product_categories,id',
'code' => "nullable|string|max:64|unique:product_categories,code,{$categoryId}",
'name' => 'nullable|string|max:191',
'description' => 'nullable|string',
'active' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'parent_id.exists' => 'La catégorie parente sélectionnée n\'existe pas.',
'code.string' => 'Le code de la catégorie doit être une chaîne de caractères.',
'code.max' => 'Le code de la catégorie ne peut pas dépasser 64 caractères.',
'code.unique' => 'Ce code de catégorie existe déjà.',
'name.string' => 'Le nom de la catégorie doit être une chaîne de caractères.',
'name.max' => 'Le nom de la catégorie ne peut pas dépasser 191 caractères.',
'description.string' => 'La description doit être une chaîne de caractères.',
'active.boolean' => 'Le statut actif doit être un booléen.',
];
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$productId = $this->route('id');
return [
'nom' => 'required|string|max:255',
'reference' => "nullable",
'categorie_id' => 'required|exists:product_categories,id',
'fabricant' => 'nullable|string|max:191',
'stock_actuel' => 'required|numeric|min:0',
'stock_minimum' => 'required|numeric|min:0',
'unite' => 'required|string|max:50',
'prix_unitaire' => 'required|numeric|min:0',
'date_expiration' => 'nullable|date|after:today',
'numero_lot' => 'nullable|string|max:100',
'conditionnement_nom' => 'nullable|string|max:191',
'conditionnement_quantite' => 'nullable|numeric|min:0',
'conditionnement_unite' => 'nullable|string|max:50',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
'remove_image' => 'nullable|boolean',
'fiche_technique_url' => 'nullable|url|max:500',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
];
}
public function messages(): array
{
return [
'nom.required' => 'Le nom du produit est obligatoire.',
'nom.string' => 'Le nom du produit doit être une chaîne de caractères.',
'nom.max' => 'Le nom du produit ne peut pas dépasser 255 caractères.',
'reference.required' => 'La référence du produit est obligatoire.',
'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_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.',
'stock_actuel.numeric' => 'Le stock actuel doit être un nombre.',
'stock_actuel.min' => 'Le stock actuel doit être supérieur ou égal à 0.',
'stock_minimum.required' => 'Le stock minimum est obligatoire.',
'stock_minimum.numeric' => 'Le stock minimum doit être un nombre.',
'stock_minimum.min' => 'Le stock minimum doit être supérieur ou égal à 0.',
'unite.required' => 'L\'unité est obligatoire.',
'unite.string' => 'L\'unité doit être une chaîne de caractères.',
'unite.max' => 'L\'unité ne peut pas dépasser 50 caractères.',
'prix_unitaire.required' => 'Le prix unitaire est obligatoire.',
'prix_unitaire.numeric' => 'Le prix unitaire doit être un nombre.',
'prix_unitaire.min' => 'Le prix unitaire doit être supérieur ou égal à 0.',
'date_expiration.date' => 'La date d\'expiration doit être une date valide.',
'date_expiration.after' => 'La date d\'expiration doit être postérieure à aujourd\'hui.',
'numero_lot.string' => 'Le numéro de lot doit être une chaîne de caractères.',
'numero_lot.max' => 'Le numéro de lot ne peut pas dépasser 100 caractères.',
'conditionnement_nom.string' => 'Le nom du conditionnement doit être une chaîne de caractères.',
'conditionnement_nom.max' => 'Le nom du conditionnement ne peut pas dépasser 191 caractères.',
'conditionnement_quantite.numeric' => 'La quantité du conditionnement doit être un nombre.',
'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.',
'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.',
'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.',
'image.image' => 'Le fichier doit être une image valide.',
'image.mimes' => 'L\'image doit être de type: jpeg, png, jpg, gif ou svg.',
'image.max' => 'L\'image ne peut pas dépasser 2MB.',
'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.',
'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.',
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
];
}
}

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' => [
'nullable',
'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,32 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'is_active' => $this->is_active,
'sort_order' => $this->sort_order,
'created_at' => $this->created_at?->toISOString(),
'updated_at' => $this->updated_at?->toISOString(),
// Relationships (loaded when needed)
// 'clients_count' => $this->whenCounted('clients'),
// 'clients' => ClientResource::collection($this->whenLoaded('clients')),
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'stats' => [
'active' => $this->collection->where('is_active', true)->count(),
'inactive' => $this->collection->where('is_active', false)->count(),
'by_type' => $this->collection->groupBy('client_category_id')->map->count(),
],
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Clients récupérés avec succès',
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientGroupCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Groupes de clients récupérés avec succès',
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientGroupResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description ?? null,
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientLocationCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'stats' => [
'default_locations' => $this->collection->where('is_default', true)->count(),
'with_gps' => $this->collection->filter(fn($location) => $location->gps_lat && $location->gps_lng)->count(),
],
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Lieux clients récupérés avec succès',
];
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientLocationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'client_id' => $this->client_id,
'name' => $this->name,
'address' => [
'line1' => $this->address_line1,
'line2' => $this->address_line2,
'postal_code' => $this->postal_code,
'city' => $this->city,
'country_code' => $this->country_code,
'full_address' => $this->full_address,
],
'gps_coordinates' => $this->gps_coordinates,
'gps_lat' => $this->gps_lat,
'gps_lng' => $this->gps_lng,
'is_default' => $this->is_default,
'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
'client' => new ClientResource($this->whenLoaded('client')),
//'interventions_as_origin' => InterventionResource::collection($this->whenLoaded('interventionsAsOrigin')),
//'transports_as_origin' => TransportResource::collection($this->whenLoaded('transportsAsOrigin')),
//'transports_as_destination' => TransportResource::collection($this->whenLoaded('transportsAsDestination')),
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Lieu client récupéré avec succès',
];
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Resources\Client;
use App\Http\Resources\Contact\ContactResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
//'company_id' => $this->company_id,
'commercial' => $this->commercial(),
'type_label' => $this->getTypeLabel(),
'name' => $this->name,
'vat_number' => $this->vat_number,
'siret' => $this->siret,
'email' => $this->email,
'phone' => $this->phone,
'billing_address' => [
'line1' => $this->billing_address_line1,
'line2' => $this->billing_address_line2,
'postal_code' => $this->billing_postal_code,
'city' => $this->billing_city,
'country_code' => $this->billing_country_code,
'full_address' => $this->billing_address,
],
'group_id' => $this->group_id,
'notes' => $this->notes,
'is_active' => $this->is_active,
// 'default_tva_rate_id' => $this->default_tva_rate_id,
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
// Counts
'contacts_count' => $this->whenCounted('contacts'),
'locations_count' => $this->whenCounted('locations'),
// 'interventions_count' => $this->whenCounted('interventions'),
// 'quotes_count' => $this->whenCounted('quotes'),
// 'invoices_count' => $this->whenCounted('invoices'),
// Relations
// 'company' => new CompanyResource($this->whenLoaded('company')),
'group' => new ClientGroupResource($this->whenLoaded('group')),
// 'default_tva_rate' => new TvaRateResource($this->whenLoaded('defaultTvaRate')),
'contacts' => ContactResource::collection($this->whenLoaded('contacts')),
'locations' => ClientLocationResource::collection($this->whenLoaded('locations')),
// 'price_lists' => PriceListResource::collection($this->whenLoaded('priceLists')),
// 'interventions' => InterventionResource::collection($this->whenLoaded('interventions')),
// 'quotes' => QuoteResource::collection($this->whenLoaded('quotes')),
// 'invoices' => InvoiceResource::collection($this->whenLoaded('invoices')),
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Resources\Contact;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ContactCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => ContactResource::collection($this->collection),
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Contacts récupérés avec succès',
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Resources\Contact;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ContactResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'client_id' => $this->client_id,
'fournisseur_id' => $this->fournisseur_id,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'full_name' => $this->full_name,
'email' => $this->email,
'phone' => $this->phone,
'role' => $this->role,
'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
'client' => $this->whenLoaded('client', function() {
return $this->client ? [
'id' => $this->client->id,
'name' => $this->client->name,
] : null;
}),
'fournisseur' => $this->whenLoaded('fournisseur', function() {
return $this->fournisseur ? [
'id' => $this->fournisseur->id,
'name' => $this->fournisseur->name,
] : null;
}),
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Contact récupéré avec succès',
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources\Deceased;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DeceasedCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem()
]
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Resources\Deceased;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DeceasedDocumentCollection 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,
'deceased_id' => $document->deceased_id,
'doc_type' => $document->doc_type,
'file_id' => $document->file_id,
'generated_at' => $document->generated_at?->format('Y-m-d H:i:s'),
'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
'deceased' => $document->deceased ? [
'id' => $document->deceased->id,
'first_name' => $document->deceased->first_name,
'last_name' => $document->deceased->last_name,
'full_name' => $document->deceased->first_name . ' ' . $document->deceased->last_name,
'date_of_birth' => $document->deceased->date_of_birth?->format('Y-m-d'),
'date_of_death' => $document->deceased->date_of_death?->format('Y-m-d'),
] : null,
'file' => $document->file ? [
'id' => $document->file->id,
'filename' => $document->file->filename ?? null,
'path' => $document->file->path ?? null,
'mime_type' => $document->file->mime_type ?? null,
'size' => $document->file->size ?? null,
] : null,
];
}),
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Resources\Deceased;
use Illuminate\Http\Resources\Json\JsonResource;
class DeceasedDocumentResource 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,
'deceased_id' => $this->deceased_id,
'doc_type' => $this->doc_type,
'file_id' => $this->file_id,
'generated_at' => $this->generated_at?->format('Y-m-d H:i:s'),
'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
'deceased' => $this->when(
$this->relationLoaded('deceased'),
function () {
return [
'id' => $this->deceased->id,
'first_name' => $this->deceased->first_name,
'last_name' => $this->deceased->last_name,
'full_name' => $this->deceased->first_name . ' ' . $this->deceased->last_name,
'date_of_birth' => $this->deceased->date_of_birth?->format('Y-m-d'),
'date_of_death' => $this->deceased->date_of_death?->format('Y-m-d'),
];
}
),
'file' => $this->when(
$this->relationLoaded('file'),
function () {
return $this->file ? [
'id' => $this->file->id,
'filename' => $this->file->filename ?? null,
'path' => $this->file->path ?? null,
'mime_type' => $this->file->mime_type ?? null,
'size' => $this->file->size ?? null,
] : null;
}
),
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Resources\Deceased;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DeceasedResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'last_name' => $this->last_name,
'first_name' => $this->first_name,
'full_name' => trim($this->first_name . ' ' . $this->last_name),
'birth_date' => $this->birth_date ? $this->birth_date->format('Y-m-d') : null,
'death_date' => $this->death_date ? $this->death_date->format('Y-m-d') : null,
'place_of_death' => $this->place_of_death,
'notes' => $this->notes,
'documents_count' => $this->documents_count ?? $this->documents()->count(),
'interventions_count' => $this->interventions_count ?? $this->interventions()->count(),
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at->format('Y-m-d H:i:s')
];
}
}

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,47 @@
<?php
namespace App\Http\Resources\Employee;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Http\Resources\Employee\EmployeeResource;
use App\Http\Resources\Employee\PractitionerDocumentResource;
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,
'employee_name' => $this->when(
$this->relationLoaded('employee'),
$this->employee->full_name ?? ($this->employee->first_name . ' ' . $this->employee->last_name)
),
'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

@ -0,0 +1,86 @@
<?php
namespace App\Http\Resources\File;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class FileCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => FileResource::collection($this->collection),
'pagination' => [
'current_page' => $this->currentPage(),
'from' => $this->firstItem(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'to' => $this->lastItem(),
'total' => $this->total(),
'has_more_pages' => $this->hasMorePages(),
],
'summary' => [
'total_files' => $this->collection->count(),
'total_size' => $this->collection->sum('size_bytes'),
'total_size_formatted' => $this->formatBytes($this->collection->sum('size_bytes')),
'categories' => $this->getCategoryStats(),
],
];
}
/**
* Calculate category statistics from the collection
*/
private function getCategoryStats(): array
{
$categories = [];
foreach ($this->collection as $file) {
$pathParts = explode('/', $file->storage_uri);
$category = $pathParts[count($pathParts) - 3] ?? 'general';
if (!isset($categories[$category])) {
$categories[$category] = [
'count' => 0,
'total_size' => 0,
'files' => []
];
}
$categories[$category]['count']++;
$categories[$category]['total_size'] += $file->size_bytes ?? 0;
$categories[$category]['files'][] = $file->file_name;
}
// Format sizes
foreach ($categories as $category => &$stats) {
$stats['total_size_formatted'] = $this->formatBytes($stats['total_size']);
// Remove file list to avoid too much data in collection
unset($stats['files']);
}
return $categories;
}
/**
* Format bytes to human readable format
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
if ($bytes === 0) {
return '0 B';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$base = 1024;
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$precision}f", $bytes / pow($base, $factor)) . ' ' . $units[$factor];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Resources\File;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FileResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'file_name' => $this->file_name,
'mime_type' => $this->mime_type,
'size_bytes' => $this->size_bytes,
'size_formatted' => $this->formatted_size,
'extension' => $this->extension,
'storage_uri' => $this->storage_uri,
'organized_path' => $this->organized_path,
'sha256' => $this->sha256,
'uploaded_by' => $this->uploaded_by,
'uploader_name' => $this->uploader_name,
'uploaded_at' => $this->uploaded_at?->format('Y-m-d H:i:s'),
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
// File type helpers
'is_image' => $this->is_image,
'is_pdf' => $this->is_pdf,
// URL for accessing the file (if public)
'url' => $this->when(
$this->is_public ?? false,
asset('storage/' . $this->storage_uri)
),
// Relations
'user' => [
'id' => $this->user?->id,
'name' => $this->user?->name,
'email' => $this->user?->email,
],
// Additional metadata from the file's path structure
'category' => $this->when(
$this->storage_uri,
function () {
$pathParts = explode('/', $this->storage_uri);
return $pathParts[count($pathParts) - 3] ?? 'general';
}
),
'subcategory' => $this->when(
$this->storage_uri,
function () {
$pathParts = explode('/', $this->storage_uri);
return $pathParts[count($pathParts) - 2] ?? 'general';
}
),
];
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Http\Resources\FileAttachment;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FileAttachmentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'file_id' => $this->file_id,
'label' => $this->label,
'sort_order' => $this->sort_order,
'attachable_type' => $this->attachable_type,
'attachable_id' => $this->attachable_id,
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
// File information
'file' => $this->whenLoaded('file', function () {
return [
'id' => $this->file->id,
'name' => $this->file->name,
'original_name' => $this->file->original_name ?? $this->file->name,
'path' => $this->file->path,
'mime_type' => $this->file->mime_type,
'size' => $this->file->size,
'size_formatted' => $this->formatFileSize($this->file->size ?? 0),
'extension' => pathinfo($this->file->name, PATHINFO_EXTENSION),
'download_url' => url('/api/files/' . $this->file->id . '/download'),
];
}),
// Attachable model information
'attachable' => $this->whenLoaded('attachable', function () {
return [
'id' => $this->attachable->id,
'type' => class_basename($this->attachable),
'name' => $this->getAttachableName(),
];
}),
// Helper methods
'is_for_intervention' => $this->isForIntervention(),
'is_for_client' => $this->isForClient(),
'is_for_deceased' => $this->isForDeceased(),
'download_url' => $this->downloadUrl,
];
}
/**
* Format file size in human readable format
*/
private function formatFileSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
/**
* Get the display name of the attached model
*/
private function getAttachableName(): string
{
if (!$this->attachable) {
return 'Unknown';
}
return match (get_class($this->attachable)) {
\App\Models\Intervention::class => $this->attachable->title ?? "Intervention #{$this->attachable->id}",
\App\Models\Client::class => $this->attachable->name ?? "Client #{$this->attachable->id}",
\App\Models\Deceased::class => $this->attachable->name ?? "Deceased #{$this->attachable->id}",
default => 'Unknown Model'
};
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Resources\Fournisseur;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class FournisseurCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'stats' => [
'active' => $this->collection->where('is_active', true)->count(),
'inactive' => $this->collection->where('is_active', false)->count(),
],
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Fournisseurs récupérés avec succès',
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Resources\Fournisseur;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FournisseurResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'commercial' => $this->commercial(),
'name' => $this->name,
'vat_number' => $this->vat_number,
'siret' => $this->siret,
'email' => $this->email,
'phone' => $this->phone,
'billing_address' => [
'line1' => $this->billing_address_line1,
'line2' => $this->billing_address_line2,
'postal_code' => $this->billing_postal_code,
'city' => $this->billing_city,
'country_code' => $this->billing_country_code,
'full_address' => $this->billing_address,
],
'notes' => $this->notes,
'is_active' => $this->is_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'),
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Resources\Intervention;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InterventionAttachmentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'intervention_id' => $this->intervention_id,
'file' => $this->whenLoaded('file', function () {
return [
'id' => $this->file->id,
'name' => $this->file->name,
'path' => $this->file->path,
'mime_type' => $this->file->mime_type,
'size' => $this->file->size
];
}),
'label' => $this->label,
'created_at' => $this->created_at->format('Y-m-d H:i:s')
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Resources\Intervention;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class InterventionCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'status_summary' => $this->calculateStatusSummary()
]
];
}
/**
* Calculate summary of intervention statuses.
*
* @return array
*/
protected function calculateStatusSummary(): array
{
$statusCounts = $this->collection->groupBy('status')
->map(function ($group) {
return $group->count();
})
->toArray();
return $statusCounts;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources\Intervention;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InterventionNotificationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'intervention_id' => $this->intervention_id,
'channel' => $this->channel,
'destination' => $this->destination,
'payload' => $this->payload,
'status' => $this->status,
'sent_at' => $this->sent_at ? $this->sent_at->format('Y-m-d H:i:s') : null,
'created_at' => $this->created_at->format('Y-m-d H:i:s')
];
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Resources\Intervention;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Http\Resources\Deceased\DeceasedResource;
use App\Http\Resources\Client\ClientResource;
use App\Http\Resources\Employee\ThanatopractitionerResource;
class InterventionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'client' => $this->whenLoaded('client', function () {
return new ClientResource($this->client);
}),
'deceased' => $this->whenLoaded('deceased', function () {
return new DeceasedResource($this->deceased);
}),
'order_giver' => $this->order_giver,
'location' => $this->whenLoaded('location', function () {
return [
'id' => $this->location->id,
'name' => $this->location->name
];
}),
'type' => $this->type,
'scheduled_at' => $this->scheduled_at ? $this->scheduled_at->format('Y-m-d H:i:s') : null,
'duration_min' => $this->duration_min,
'status' => $this->status,
'practitioners' => $this->whenLoaded('practitioners', function () {
return $this->practitioners->map(function ($practitioner) {
return [
'id' => $practitioner->id,
'employee_id' => $practitioner->employee_id,
'employee_name' => $practitioner->employee->full_name ?? ($practitioner->employee->first_name . ' ' . $practitioner->employee->last_name),
'diploma_number' => $practitioner->diploma_number,
'diploma_date' => $practitioner->diploma_date?->format('Y-m-d'),
'authorization_number' => $practitioner->authorization_number,
'authorization_issue_date' => $practitioner->authorization_issue_date?->format('Y-m-d'),
'authorization_expiry_date' => $practitioner->authorization_expiry_date?->format('Y-m-d'),
'notes' => $practitioner->notes,
'is_authorization_valid' => $practitioner->is_authorization_valid,
'created_at' => $practitioner->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $practitioner->updated_at?->format('Y-m-d H:i:s'),
'role' => $practitioner->pivot->role ?? null,
];
});
}),
'principal_practitioner' => $this->whenLoaded('practitioners', function () {
$principal = $this->practitioners->where('pivot.role', 'principal')->first();
if (!$principal) {
return null;
}
return [
'id' => $principal->id,
'employee_id' => $principal->employee_id,
'employee_name' => $principal->employee->full_name ?? ($principal->employee->first_name . ' ' . $principal->employee->last_name),
'diploma_number' => $principal->diploma_number,
'diploma_date' => $principal->diploma_date?->format('Y-m-d'),
'authorization_number' => $principal->authorization_number,
'authorization_issue_date' => $principal->authorization_issue_date?->format('Y-m-d'),
'authorization_expiry_date' => $principal->authorization_expiry_date?->format('Y-m-d'),
'notes' => $principal->notes,
'is_authorization_valid' => $principal->is_authorization_valid,
'created_at' => $principal->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $principal->updated_at?->format('Y-m-d H:i:s'),
'role' => $principal->pivot->role ?? null,
];
}),
'attachments_count' => $this->attachments_count,
'notes' => $this->notes,
'created_by' => $this->created_by,
'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
'attachments' => $this->whenLoaded('attachments', function () {
return InterventionAttachmentResource::collection($this->attachments);
}),
'notifications' => $this->whenLoaded('notifications', function () {
return InterventionNotificationResource::collection($this->notifications);
})
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Resources\Product;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProductCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'pagination' => [
'current_page' => $this->currentPage(),
'from' => $this->firstItem(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'to' => $this->lastItem(),
'total' => $this->total(),
],
'summary' => [
'total_products' => $this->collection->count(),
'low_stock_products' => $this->collection->filter(function ($product) {
return $product->stock_actuel <= $product->stock_minimum;
})->count(),
'total_value' => $this->collection->sum(function ($product) {
return $product->stock_actuel * $product->prix_unitaire;
}),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Produits récupérés avec succès',
];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Http\Resources\Product;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'nom' => $this->nom,
'reference' => $this->reference,
'categorie_id' => $this->categorie_id,
'fabricant' => $this->fabricant,
'stock_actuel' => $this->stock_actuel,
'stock_minimum' => $this->stock_minimum,
'unite' => $this->unite,
'prix_unitaire' => $this->prix_unitaire,
'date_expiration' => $this->date_expiration?->format('Y-m-d'),
'numero_lot' => $this->numero_lot,
'conditionnement' => [
'nom' => $this->conditionnement_nom,
'quantite' => $this->conditionnement_quantite,
'unite' => $this->conditionnement_unite,
],
'media' => [
'photo_url' => $this->photo_url,
'fiche_technique_url' => $this->fiche_technique_url,
],
'is_low_stock' => $this->stock_actuel <= $this->stock_minimum,
'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
'fournisseur' => $this->whenLoaded('fournisseur', function() {
return $this->fournisseur ? [
'id' => $this->fournisseur->id,
'name' => $this->fournisseur->name,
'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;
}),
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Produit récupéré avec succès',
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Resources\ProductCategory;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProductCategoryCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
if ($this->resource instanceof \Illuminate\Pagination\LengthAwarePaginator) {
return [
'data' => ProductCategoryResource::collection($this->collection),
'pagination' => [
'current_page' => $this->currentPage(),
'from' => $this->firstItem(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'to' => $this->lastItem(),
'total' => $this->total(),
],
];
}
return [
'data' => ProductCategoryResource::collection($this->collection),
'count' => $this->collection->count(),
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Resources\ProductCategory;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'parent_id' => $this->parent_id,
'code' => $this->code,
'name' => $this->name,
'description' => $this->description,
'active' => $this->active,
'path' => $this->path,
'has_children' => $this->hasChildren(),
'has_products' => $this->hasProducts(),
'children_count' => $this->children()->count(),
'products_count' => $this->products()->count(),
// Parent information
'parent' => $this->whenLoaded('parent', function () {
return new ProductCategoryResource($this->parent);
}),
// Children information
'children' => $this->whenLoaded('children', function () {
return ProductCategoryResource::collection($this->children);
}),
// Relationships
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Client extends Model
{
protected $fillable = [
'name',
'vat_number',
'siret',
'email',
'phone',
'billing_address_line1',
'billing_address_line2',
'billing_postal_code',
'billing_city',
'billing_country_code',
'group_id',
'notes',
'is_active',
// 'default_tva_rate_id',
'client_category_id',
'user_id',
];
protected $casts = [
'is_active' => 'boolean',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function commercial(): ?string
{
return $this->user ? $this->user->name : 'Système';
}
public function category(): BelongsTo
{
return $this->belongsTo(ClientCategory::class, 'client_category_id');
}
/**
* Get the human-readable label for the client type.
*/
public function getTypeLabel(): string
{
return $this->category ? $this->category->name : 'Non catégorisé';
}
/**
* Get the full billing address as a string.
*/
public function getBillingAddressAttribute(): ?string
{
$parts = array_filter([
$this->billing_address_line1,
$this->billing_address_line2,
$this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city,
$this->billing_country_code,
]);
return !empty($parts) ? implode(', ', $parts) : null;
}
/**
* Get the file attachments for the client (polymorphic).
*/
public function fileAttachments()
{
return $this->morphMany(FileAttachment::class, 'attachable')->orderBy('sort_order');
}
/**
* Get the files attached to this client.
*/
public function attachedFiles()
{
return $this->fileAttachments()->with('file');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ClientCategory extends Model
{
protected $fillable = [
'name',
'slug',
'description',
'is_active',
'sort_order',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Get the clients for the category.
*/
public function clients(): HasMany
{
return $this->hasMany(Client::class);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientContact extends Model
{
//
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientGroup extends Model
{
protected $fillable = [
'name',
'description',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientLocation extends Model
{
protected $fillable = [
'client_id',
'name',
'address_line1',
'address_line2',
'postal_code',
'city',
'country_code',
'gps_lat',
'gps_lng',
'is_default',
];
protected $casts = [
'is_default' => 'boolean',
'gps_lat' => 'decimal:8',
'gps_lng' => 'decimal:8',
];
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Contact extends Model
{
protected $fillable = [
'first_name',
'last_name',
'email',
'phone',
'mobile',
'position',
'notes',
'is_primary',
'client_id',
'fournisseur_id'
];
protected $casts = [
'is_primary' => 'boolean',
];
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
public function fournisseur(): BelongsTo
{
return $this->belongsTo(Fournisseur::class);
}
/**
* Get the contact's full name.
*/
public function getFullNameAttribute(): string
{
return trim("{$this->first_name} {$this->last_name}");
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Deceased extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'deceased';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'last_name',
'first_name',
'birth_date',
'death_date',
'place_of_death',
'notes'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'birth_date' => 'date',
'death_date' => 'date',
];
/**
* Get the documents associated with the deceased.
*/
public function documents(): HasMany
{
return $this->hasMany(DeceasedDocument::class);
}
/**
* Get the interventions associated with the deceased.
*/
public function interventions(): HasMany
{
return $this->hasMany(Intervention::class);
}
/**
* Get the file attachments for the deceased (polymorphic).
*/
public function fileAttachments()
{
return $this->morphMany(FileAttachment::class, 'attachable')->orderBy('sort_order');
}
/**
* Get the files attached to this deceased.
*/
public function attachedFiles()
{
return $this->fileAttachments()->with('file');
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DeceasedDocument extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'deceased_id',
'doc_type',
'file_id',
'generated_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'generated_at' => 'datetime'
];
/**
* Get the deceased associated with the document.
*/
public function deceased(): BelongsTo
{
return $this->belongsTo(Deceased::class);
}
/**
* Get the file associated with the document.
*/
public function file(): BelongsTo
{
return $this->belongsTo(File::class);
}
}

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,98 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class File extends Model
{
protected $fillable = [
'file_name',
'mime_type',
'size_bytes',
'storage_uri',
'sha256',
'uploaded_by',
'uploaded_at',
];
protected $casts = [
'size_bytes' => 'integer',
'uploaded_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'uploaded_by');
}
/**
* Get the uploader name.
*/
public function getUploaderName(): string
{
return $this->user ? $this->user->name : 'Utilisateur inconnu';
}
/**
* Get the formatted file size.
*/
public function getFormattedSize(): string
{
if (!$this->size_bytes) {
return '0 B';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = $this->size_bytes;
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
/**
* Get the file extension from the file name.
*/
public function getExtension(): string
{
return pathinfo($this->file_name, PATHINFO_EXTENSION);
}
/**
* Check if the file is an image.
*/
public function isImage(): bool
{
return str_starts_with($this->mime_type ?? '', 'image/');
}
/**
* Check if the file is a PDF.
*/
public function isPdf(): bool
{
return $this->mime_type === 'application/pdf';
}
/**
* Get the organized storage path (e.g., client/devis/filename.pdf).
*/
public function getOrganizedPath(): string
{
// Extract directory structure from storage_uri
$path = $this->storage_uri;
// Remove storage path prefix if present
if (str_contains($path, 'storage/')) {
$path = substr($path, strpos($path, 'storage/') + 8);
}
return $path;
}
}

View File

@ -0,0 +1,141 @@
<?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\MorphTo;
class FileAttachment extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'file_attachments';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'file_id',
'label',
'attachable_type',
'attachable_id',
'sort_order',
'created_at',
'updated_at',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'sort_order' => 'integer',
];
/**
* Get the parent attachable model (polymorphic).
*/
public function attachable(): MorphTo
{
return $this->morphTo();
}
/**
* Get the file associated with the attachment.
*/
public function file(): BelongsTo
{
return $this->belongsTo(File::class);
}
/**
* Get the intervention associated with the attachment (legacy support).
*/
public function intervention(): BelongsTo
{
return $this->belongsTo(Intervention::class, 'attachable_id')->where('attachable_type', Intervention::class);
}
/**
* Get the client associated with the attachment.
*/
public function client(): BelongsTo
{
return $this->belongsTo(Client::class, 'attachable_id')->where('attachable_type', Client::class);
}
/**
* Get the deceased associated with the attachment.
*/
public function deceased(): BelongsTo
{
return $this->belongsTo(Deceased::class, 'attachable_id')->where('attachable_type', Deceased::class);
}
/**
* Scope to filter by attachable type.
*/
public function scopeOfType($query, string $type)
{
return $query->where('attachable_type', $type);
}
/**
* Scope to filter by attachable model.
*/
public function scopeFor($query, Model $model)
{
return $query->where('attachable_type', get_class($model))
->where('attachable_id', $model->getKey());
}
/**
* Get the display name of the attached model.
*/
public function getAttachableNameAttribute(): string
{
return $this->attachable?->name ?? $this->attachable?->file_name ?? 'Unknown';
}
/**
* Get the URL for downloading the attached file.
*/
public function getDownloadUrlAttribute(): string
{
return url('/api/files/' . $this->file_id . '/download');
}
/**
* Check if this attachment belongs to an intervention.
*/
public function isForIntervention(): bool
{
return $this->attachable_type === Intervention::class;
}
/**
* Check if this attachment belongs to a client.
*/
public function isForClient(): bool
{
return $this->attachable_type === Client::class;
}
/**
* Check if this attachment belongs to a deceased.
*/
public function isForDeceased(): bool
{
return $this->attachable_type === Deceased::class;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Fournisseur extends Model
{
protected $fillable = [
'name',
'vat_number',
'siret',
'email',
'phone',
'billing_address_line1',
'billing_address_line2',
'billing_postal_code',
'billing_city',
'billing_country_code',
'notes',
'is_active',
'user_id',
];
protected $casts = [
'is_active' => 'boolean',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function commercial(): ?string
{
return $this->user ? $this->user->name : 'Système';
}
/**
* Get the full billing address as a string.
*/
public function getBillingAddressAttribute(): ?string
{
$parts = array_filter([
$this->billing_address_line1,
$this->billing_address_line2,
$this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city,
$this->billing_country_code,
]);
return !empty($parts) ? implode(', ', $parts) : null;
}
}

View File

@ -0,0 +1,147 @@
<?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\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Intervention extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'client_id',
'deceased_id',
'order_giver',
'location_id',
'type',
'scheduled_at',
'duration_min',
'status',
'attachments_count',
'notes',
'created_by'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'scheduled_at' => 'datetime',
'attachments_count' => 'integer'
];
/**
* Get the client associated with the intervention.
*/
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
/**
* Get the deceased associated with the intervention.
*/
public function deceased(): BelongsTo
{
return $this->belongsTo(Deceased::class);
}
/**
* Get the location associated with the intervention.
*/
public function location(): BelongsTo
{
return $this->belongsTo(ClientLocation::class);
}
/**
* Get the practitioners assigned to the intervention.
*/
public function practitioners(): BelongsToMany
{
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->withPivot('role', 'assigned_at')
->withTimestamps();
}
/**
* Alias for practitioners relationship (for backward compatibility).
*/
public function assignedPractitioner(): BelongsToMany
{
return $this->practitioners();
}
/**
* Get the principal practitioner assigned to the intervention.
*/
public function principalPractitioner(): BelongsToMany
{
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'principal')
->withPivot('role', 'assigned_at')
->withTimestamps();
}
/**
* Get the assistant practitioners assigned to the intervention.
*/
public function assistantPractitioners(): BelongsToMany
{
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
->wherePivot('role', 'assistant')
->withPivot('role', 'assigned_at')
->withTimestamps();
}
/**
* Get the user who created the intervention.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get the attachments for the intervention (legacy support).
*/
public function attachments(): HasMany
{
return $this->hasMany(InterventionAttachment::class);
}
/**
* Get the file attachments for the intervention (polymorphic).
*/
public function fileAttachments()
{
return $this->morphMany(FileAttachment::class, 'attachable')->orderBy('sort_order');
}
/**
* Get the files attached to this intervention.
*/
public function attachedFiles()
{
return $this->fileAttachments()->with('file');
}
/**
* Get the notifications for the intervention.
*/
public function notifications(): HasMany
{
return $this->hasMany(InterventionNotification::class);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InterventionAttachment extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'intervention_id',
'file_id',
'label'
];
/**
* Get the intervention associated with the attachment.
*/
public function intervention(): BelongsTo
{
return $this->belongsTo(Intervention::class);
}
/**
* Get the file associated with the attachment.
*/
public function file(): BelongsTo
{
return $this->belongsTo(File::class);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InterventionNotification extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'intervention_id',
'channel',
'destination',
'payload',
'status',
'sent_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'payload' => 'array',
'sent_at' => 'datetime'
];
/**
* Get the intervention associated with the notification.
*/
public function intervention(): BelongsTo
{
return $this->belongsTo(Intervention::class);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InterventionPractitioner extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'intervention_practitioner';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'intervention_id',
'practitioner_id',
'role',
'assigned_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'assigned_at' => 'datetime'
];
/**
* Get the intervention that owns the practitioner assignment.
*/
public function intervention(): BelongsTo
{
return $this->belongsTo(Intervention::class);
}
/**
* Get the practitioner assigned to the intervention.
*/
public function practitioner(): BelongsTo
{
return $this->belongsTo(Thanatopractitioner::class, 'practitioner_id');
}
/**
* Scope to get principal practitioners.
*/
public function scopePrincipal($query)
{
return $query->where('role', 'principal');
}
/**
* Scope to get assistant practitioners.
*/
public function scopeAssistant($query)
{
return $query->where('role', 'assistant');
}
}

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

Some files were not shown because too many files have changed in this diff Show More