Compare commits

..

69 Commits

Author SHA1 Message Date
nyavokevin
8f7019e815 feat(invoice): support group-based invoices without a client
Allow invoices to target either a client or a client group by making
`client_id` nullable and validating that exactly one recipient is set.

Load group relations in invoice data so the frontend can display group
details in invoice views and type definitions.

Make quote acceptance reuse an existing invoice instead of creating a
duplicate, and surface backend status update errors in the quote UI.
2026-04-02 16:08:44 +03:00
nyavokevin
9cbc1bcbdb feat(ui): add price lists and group-based quote flows
Add price list management across the API, store, services, routes,
navigation, and sales views.

Support quotes for either a client or a client group, including PDF
download and nullable client validation for group-based recipients.

Extend client groups to manage assigned clients directly from the form
and detail views, and refresh supplier, intervention, stock, and order
screens with updated interactions and layouts.
2026-04-02 12:07:11 +03:00
nyavokevin
dd6fc4665c Feat: redesing form on new 2026-03-24 14:19:49 +03:00
nyavokevin
ebd171e9de Link internvetion and invoice and quote 2026-03-17 16:30:02 +03:00
nyavokevin
8ee7d8f8e9 Feat: redesign facture comme dans le facutre 2026-03-16 17:13:10 +03:00
nyavokevin
8171a20d41 Fix nom lieu internvetion 2026-03-13 16:34:44 +03:00
nyavokevin
bd04e07f12 Ameloration design 2026-03-13 16:13:49 +03:00
nyavokevin
dec87dfdb7 Feat: improve desing contact and agenda 2026-03-05 17:12:40 +03:00
nyavokevin
8074ac4f48 FIX: Creation demande, client on clique show 2026-03-05 17:00:32 +03:00
nyavokevin
11750a3ffc Ventes et modules transverses: devis, factures, messages, stats et webmailing 2026-03-02 15:46:38 +03:00
nyavokevin
dc87b0f720 Avoirs et factures fournisseur: harmonisation des écrans, formulaires et stores 2026-03-02 15:46:25 +03:00
nyavokevin
ecfe25d3ca CRM: refonte clients, fournisseurs et groupes clients 2026-03-02 15:46:08 +03:00
nyavokevin
083f78673e Planning et agenda: nouveau flux de création et formulaires de demande 2026-03-02 15:45:50 +03:00
nyavokevin
a9a2429b67 Stock et achats: amélioration des réceptions, entrepôts et commandes fournisseurs 2026-03-02 15:45:35 +03:00
kevin
094c7a0980 Nouvel style planning 2026-02-04 13:54:06 +03:00
kevin
31090d12ba Gestion des bon de receptions dans front 2026-02-03 15:30:27 +03:00
kevin
d8927580e7 Feature: Warehouse et aussi les API bon de reception et Stock, mouvement stock, Warehouse stock 2026-02-02 17:02:23 +03:00
kevin
4af8ea2c60 Gestion des avoirs 2026-01-29 16:44:31 +03:00
kevin
c0868b6acb Fix repertroire fournisseur 2026-01-29 11:56:38 +03:00
kevin
5a2b1684aa Repository fix 2026-01-28 15:36:37 +03:00
kevin
44681da674 Fix Dossier 2026-01-28 15:23:54 +03:00
kevin
d57e9a1a67 Fix build 2026-01-28 14:58:59 +03:00
kevin
ed5181d290 Feat purchase order 2026-01-28 14:26:20 +03:00
kevin
0009eb8c86 FEAT: service and store bon commande, receiption, fournisseur 2026-01-27 08:53:26 +03:00
kevin
3bc4178a12 migration Commandes fournisseurs, les Lignes de commandes de marchandises, Lignes de réception, Factures fournisseurs 2026-01-26 12:02:28 +03:00
kevin
f62a2db36e Feat, design, liste avoir, details avoirs, creation avoir, liste commande fournisseur, details commande fournisseur, creation commande fournisseur, webmail 2026-01-23 16:13:03 +03:00
kevin
86472e0de9 Fix intervention modal 2026-01-20 17:32:50 +03:00
kevin
16a39014a2 Change select internvetion, defunt, client, et produit 2026-01-19 17:52:33 +03:00
kevin
39a3062009 FIX: uppercase 2026-01-12 17:12:02 +03:00
kevin
c00ce5ab94 FEAT: activite client= 2026-01-12 16:37:41 +03:00
kevin
503fb0d008 quotes: Generer des factures en acceptant un devis 2026-01-09 18:02:15 +03:00
kevin
e0ccd5f627 Feat: migration client table, modification groupe client, child dans client 2026-01-08 17:18:49 +03:00
kevin
50f79a8040 feat: Introduce client group management and enhance quote creation and detail views. 2026-01-07 18:17:11 +03:00
kevin
d911435b5c feat: Implement comprehensive quote and quote line management with new backend API and frontend UI. 2026-01-06 16:50:03 +03:00
kevin
19b592720e feat: Implement full CRUD API for quotes with dedicated model, repository, requests, resource, and database migration. 2026-01-06 13:59:35 +03:00
kevin
5d93f9d39a feat: Implement product category management and a multi-step intervention wizard with product selection, including new database migrations and CORS configuration updates. 2026-01-05 17:21:02 +03:00
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
798 changed files with 109647 additions and 1072 deletions

2
.gitignore vendored
View File

@ -20,6 +20,8 @@
*.DS_Store *.DS_Store
*.idea/ *.idea/
*.vscode/ *.vscode/
node_modules/
*.env.local *.env.local
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View File

@ -0,0 +1,27 @@
{
"$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json",
"quiet_mode": false,
"debug": false,
"auto_update": true,
"keep_thinking": false,
"session_recovery": true,
"auto_resume": true,
"resume_text": "continue",
"empty_response_max_attempts": 4,
"empty_response_retry_delay_ms": 2000,
"tool_id_recovery": true,
"claude_tool_hardening": true,
"proactive_token_refresh": true,
"proactive_refresh_buffer_seconds": 1800,
"proactive_refresh_check_interval_seconds": 300,
"max_rate_limit_wait_seconds": 300,
"quota_fallback": false,
"account_selection_strategy": "sticky",
"pid_offset_enabled": false,
"signature_cache": {
"enabled": true,
"memory_ttl_seconds": 3600,
"disk_ttl_seconds": 172800,
"write_interval_seconds": 60
}
}

73
.opencode/opencode.json Normal file
View File

@ -0,0 +1,73 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-antigravity-auth@beta"],
"provider": {
"google": {
"models": {
"antigravity-gemini-3-pro": {
"name": "Gemini 3 Pro (Antigravity)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingLevel": "low" },
"high": { "thinkingLevel": "high" }
}
},
"antigravity-gemini-3-flash": {
"name": "Gemini 3 Flash (Antigravity)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"minimal": { "thinkingLevel": "minimal" },
"low": { "thinkingLevel": "low" },
"medium": { "thinkingLevel": "medium" },
"high": { "thinkingLevel": "high" }
}
},
"antigravity-claude-sonnet-4-5": {
"name": "Claude Sonnet 4.5 (Antigravity)",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"antigravity-claude-sonnet-4-5-thinking": {
"name": "Claude Sonnet 4.5 Thinking (Antigravity)",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
},
"antigravity-claude-opus-4-5-thinking": {
"name": "Claude Opus 4.5 Thinking (Antigravity)",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
},
"gemini-2.5-flash": {
"name": "Gemini 2.5 Flash (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-2.5-pro": {
"name": "Gemini 2.5 Pro (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-3-flash-preview": {
"name": "Gemini 3 Flash Preview (Gemini CLI)",
"limit": { "context": 1048576, "output": 65536 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"gemini-3-pro-preview": {
"name": "Gemini 3 Pro Preview (Gemini CLI)",
"limit": { "context": 1048576, "output": 65535 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
}
}
}
}
}

1000
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"exceljs": "^4.4.0"
}
}

View File

@ -1,65 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=thanasoft_back
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

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,151 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreAvoirRequest;
use App\Http\Requests\UpdateAvoirRequest;
use App\Http\Resources\AvoirResource;
use App\Repositories\AvoirRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class AvoirController extends Controller
{
public function __construct(
protected AvoirRepositoryInterface $avoirRepository
) {
}
/**
* Display a listing of credits.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$avoirs = $this->avoirRepository->all();
return AvoirResource::collection($avoirs);
} catch (\Exception $e) {
Log::error('Error fetching avoirs: ' . $e->getMessage(), [
'exception' => $e,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des avoirs.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created credit.
*/
public function store(StoreAvoirRequest $request): AvoirResource|JsonResponse
{
try {
$avoir = $this->avoirRepository->create($request->validated());
return new AvoirResource($avoir);
} catch (\Exception $e) {
Log::error('Error creating avoir: ' . $e->getMessage(), [
'exception' => $e,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'avoir.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified credit.
*/
public function show(string $id): AvoirResource|JsonResponse
{
try {
$avoir = $this->avoirRepository->find($id);
if (!$avoir) {
return response()->json([
'message' => 'Avoir non trouvé.',
], 404);
}
return new AvoirResource($avoir);
} catch (\Exception $e) {
Log::error('Error fetching avoir: ' . $e->getMessage(), [
'exception' => $e,
'avoir_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de l\'avoir.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified credit.
*/
public function update(UpdateAvoirRequest $request, string $id): AvoirResource|JsonResponse
{
try {
$updated = $this->avoirRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Avoir non trouvé ou échec de la mise à jour.',
], 404);
}
$avoir = $this->avoirRepository->find($id);
return new AvoirResource($avoir);
} catch (\Exception $e) {
Log::error('Error updating avoir: ' . $e->getMessage(), [
'exception' => $e,
'avoir_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de l\'avoir.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified credit.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->avoirRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Avoir non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Avoir supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting avoir: ' . $e->getMessage(), [
'exception' => $e,
'avoir_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de l\'avoir.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Client;
use App\Repositories\ClientActivityTimelineRepositoryInterface;
use App\Http\Resources\Client\ClientActivityTimelineResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ClientActivityTimelineController extends Controller
{
protected $repository;
public function __construct(ClientActivityTimelineRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Get activity timeline for a client
*/
public function index(Request $request, Client $client)
{
try {
$perPage = (int) $request->get('per_page', 10);
$activities = $this->repository->getByClient($client->id, $perPage);
return ClientActivityTimelineResource::collection($activities);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error fetching client timeline: ' . $e->getMessage()
], 500);
}
}
}

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,339 @@
<?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;
use Illuminate\Support\Facades\Storage;
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 = (int) $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 {
$validatedData = $request->validated();
$client = $this->clientRepository->find($id);
if (!$client) {
return response()->json([
'message' => 'Client non trouvé.',
], 404);
}
if ($request->hasFile('avatar')) {
// Delete old avatar if exists
if ($client->avatar) {
Storage::disk('public')->delete($client->avatar);
}
// Store new avatar
$path = $request->file('avatar')->store('avatars', 'public');
$validatedData['avatar'] = $path;
}
$updated = $this->clientRepository->update($id, $validatedData);
if (!$updated) {
return response()->json([
'message' => 'Échec de la mise à jour.',
], 500);
}
$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);
}
}
/**
* Change client status (active/inactive).
*/
public function changeStatus(Request $request, string $id): ClientResource|JsonResponse
{
try {
$isActive = $request->input('is_active');
$updated = $this->clientRepository->update($id, ['is_active' => $isActive]);
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 changing client status: ' . $e->getMessage(), [
'exception' => $e,
'client_id' => $id,
]);
return response()->json(['message' => 'Erreur serveur'], 500);
}
}
/**
* Get children clients.
*/
public function getChildren(string $id): AnonymousResourceCollection|JsonResponse
{
try {
$client = $this->clientRepository->find($id);
if (!$client) {
return response()->json(['message' => 'Client not found'], 404);
}
// Assuming the relationship is defined in the model as 'children'
$children = $client->children;
return ClientResource::collection($children);
} catch (\Exception $e) {
Log::error('Error fetching children: ' . $e->getMessage(), [
'exception' => $e,
'client_id' => $id,
]);
return response()->json(['message' => 'Erreur serveur'], 500);
}
}
/**
* Add a child client.
*/
public function addChild(string $id, string $childId): JsonResponse
{
try {
$parent = $this->clientRepository->find($id);
$child = $this->clientRepository->find($childId);
if (!$parent || !$child) {
return response()->json(['message' => 'Parent or Child not found'], 404);
}
// Update child's parent_id
$this->clientRepository->update($childId, ['parent_id' => $id]);
return response()->json(['message' => 'Child added successfully'], 200);
} catch (\Exception $e) {
Log::error('Error adding child: ' . $e->getMessage(), [
'exception' => $e,
'parent_id' => $id,
'child_id' => $childId
]);
return response()->json(['message' => 'Erreur serveur'], 500);
}
}
/**
* Remove a child client.
*/
public function removeChild(string $id, string $childId): JsonResponse
{
try {
$child = $this->clientRepository->find($childId);
if (!$child) {
return response()->json(['message' => 'Child not found'], 404);
}
if ($child->parent_id != $id) {
return response()->json(['message' => 'Client is not a child of this parent'], 400);
}
// Remove parent_id
$this->clientRepository->update($childId, ['parent_id' => null]);
return response()->json(['message' => 'Child removed successfully'], 200);
} catch (\Exception $e) {
Log::error('Error removing child: ' . $e->getMessage(), [
'exception' => $e,
'parent_id' => $id,
'child_id' => $childId
]);
return response()->json(['message' => 'Erreur serveur'], 500);
}
}
}

View File

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssignClientsToGroupRequest;
use App\Http\Requests\StoreClientGroupRequest;
use App\Http\Requests\UpdateClientGroupRequest;
use App\Http\Resources\Client\ClientGroupResource;
use App\Models\Client;
use App\Repositories\ClientGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
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 {
$validated = $request->validated();
$clientGroup = DB::transaction(function () use ($validated) {
$clientIds = Arr::get($validated, 'client_ids', []);
$clientGroup = $this->clientGroupRepository->create(Arr::except($validated, ['client_ids']));
if (!empty($clientIds)) {
Client::query()
->whereIn('id', $clientIds)
->update(['group_id' => $clientGroup->id]);
}
return $clientGroup->load(['clients'])->loadCount('clients');
});
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);
}
$clientGroup->load(['clients'])->loadCount('clients');
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 {
$validated = $request->validated();
$updated = DB::transaction(function () use ($id, $validated) {
$updated = $this->clientGroupRepository->update($id, Arr::except($validated, ['client_ids']));
if (!$updated) {
return false;
}
if (array_key_exists('client_ids', $validated)) {
$clientIds = $validated['client_ids'] ?? [];
Client::query()
->where('group_id', (int) $id)
->whereNotIn('id', $clientIds)
->update(['group_id' => null]);
if (!empty($clientIds)) {
Client::query()
->whereIn('id', $clientIds)
->update(['group_id' => (int) $id]);
}
}
return true;
});
if (!$updated) {
return response()->json([
'message' => 'Groupe de clients non trouvé ou échec de la mise à jour.',
], 404);
}
$clientGroup = $this->clientGroupRepository->find($id);
$clientGroup?->load(['clients'])->loadCount('clients');
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);
}
}
/**
* Assign many clients to one client group.
*/
public function assignClients(AssignClientsToGroupRequest $request, string $id): JsonResponse
{
try {
$clientGroup = $this->clientGroupRepository->find($id);
if (!$clientGroup) {
return response()->json([
'message' => 'Groupe de clients non trouvé.',
], 404);
}
$clientIds = $request->validated('client_ids');
$updatedCount = DB::transaction(function () use ($clientIds, $clientGroup) {
return Client::query()
->whereIn('id', $clientIds)
->update(['group_id' => $clientGroup->id]);
});
$clientGroup->load(['clients'])->loadCount('clients');
return response()->json([
'message' => 'Clients assignés au groupe avec succès.',
'assigned_count' => $updatedCount,
'group' => new ClientGroupResource($clientGroup),
]);
} catch (\Exception $e) {
Log::error('Error assigning clients to 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 lassignation des clients au groupe.',
'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 = (int) $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,159 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreGoodsReceiptRequest;
use App\Http\Requests\UpdateGoodsReceiptRequest;
use App\Http\Resources\GoodsReceiptResource;
use App\Models\PurchaseOrder;
use App\Repositories\GoodsReceiptRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class GoodsReceiptController extends Controller
{
public function __construct(
private readonly GoodsReceiptRepositoryInterface $goodsReceiptRepository
) {
}
/**
* Display a listing of goods receipts.
*/
public function index(): JsonResponse
{
try {
$goodsReceipts = $this->goodsReceiptRepository->all();
return response()->json([
'data' => GoodsReceiptResource::collection($goodsReceipts),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching goods receipts: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des réceptions de marchandises.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created goods receipt.
*/
public function store(StoreGoodsReceiptRequest $request): JsonResponse
{
try {
$payload = $request->validated();
if (empty($payload['lines']) && !empty($payload['purchase_order_id'])) {
$purchaseOrder = PurchaseOrder::query()
->with('lines')
->find($payload['purchase_order_id']);
if ($purchaseOrder) {
$payload['lines'] = $purchaseOrder->lines
->filter(fn($line) => !empty($line->product_id))
->map(fn($line) => [
'product_id' => (int) $line->product_id,
'packaging_id' => null,
'packages_qty_received' => null,
'units_qty_received' => (float) $line->quantity,
'qty_received_base' => (float) $line->quantity,
'unit_price' => (float) $line->unit_price,
'unit_price_per_package' => null,
'tva_rate_id' => null,
])
->values()
->all();
}
}
$goodsReceipt = $this->goodsReceiptRepository->create($payload);
return response()->json([
'data' => new GoodsReceiptResource($goodsReceipt),
'message' => 'Réception de marchandise créée avec succès.',
'status' => 'success'
], 201);
} catch (\Exception $e) {
Log::error('Error creating goods receipt: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la création de la réception de marchandise.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified goods receipt.
*/
public function show(string $id): JsonResponse
{
try {
$goodsReceipt = $this->goodsReceiptRepository->find((int) $id);
if (!$goodsReceipt) {
return response()->json(['message' => 'Réception de marchandise non trouvée.'], 404);
}
return response()->json([
'data' => new GoodsReceiptResource($goodsReceipt),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching goods receipt: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la réception de marchandise.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified goods receipt.
*/
public function update(UpdateGoodsReceiptRequest $request, string $id): JsonResponse
{
try {
$updated = $this->goodsReceiptRepository->update((int) $id, $request->validated());
if (!$updated) {
return response()->json(['message' => 'Réception de marchandise non trouvée ou échec de la mise à jour.'], 404);
}
$goodsReceipt = $this->goodsReceiptRepository->find((int) $id);
return response()->json([
'data' => new GoodsReceiptResource($goodsReceipt),
'message' => 'Réception de marchandise mise à jour avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error updating goods receipt: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de la réception de marchandise.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified goods receipt.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->goodsReceiptRepository->delete((int) $id);
if (!$deleted) {
return response()->json(['message' => 'Réception de marchandise non trouvée ou échec de la suppression.'], 404);
}
return response()->json([
'message' => 'Réception de marchandise supprimée avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error deleting goods receipt: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de la réception de marchandise.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,713 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInterventionRequest;
use App\Http\Requests\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 App\Repositories\QuoteRepositoryInterface;
use App\Repositories\ProductRepositoryInterface;
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;
/**
* @var QuoteRepositoryInterface
*/
protected $quoteRepository;
/**
* @var ProductRepositoryInterface
*/
protected $productRepository;
/**
* InterventionController constructor.
*
* @param InterventionRepositoryInterface $interventionRepository
* @param InterventionPractitionerRepositoryInterface $interventionPractitionerRepository
* @param ClientRepositoryInterface $clientRepository
* @param ContactRepositoryInterface $contactRepository
* @param DeceasedRepositoryInterface $deceasedRepository
* @param QuoteRepositoryInterface $quoteRepository
* @param ProductRepositoryInterface $productRepository
*/
public function __construct(
InterventionRepositoryInterface $interventionRepository,
InterventionPractitionerRepositoryInterface $interventionPractitionerRepository,
ClientRepositoryInterface $clientRepository,
ContactRepositoryInterface $contactRepository,
DeceasedRepositoryInterface $deceasedRepository,
QuoteRepositoryInterface $quoteRepository,
ProductRepositoryInterface $productRepository,
private \App\Repositories\ClientLocationRepositoryInterface $clientLocationRepository
)
{
$this->interventionRepository = $interventionRepository;
$this->interventionPractitionerRepository = $interventionPractitionerRepository;
$this->clientRepository = $clientRepository;
$this->contactRepository = $contactRepository;
$this->deceasedRepository = $deceasedRepository;
$this->quoteRepository = $quoteRepository;
$this->productRepository = $productRepository;
}
/**
* 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: Handle Deceased (Create or Link)
$deceased = null;
if (!empty($validated['deceased_id'])) {
$deceased = $this->deceasedRepository->findById($validated['deceased_id']);
}
else {
$deceasedData = $validated['deceased'];
$deceased = $this->deceasedRepository->create($deceasedData);
}
// Step 2: Link existing client or create a new one
if (!empty($validated['client_id'])) {
$client = $this->clientRepository->find($validated['client_id']);
}
else {
$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: Handle Location
$locationData = $validated['location'] ?? [];
$locationId = $validated['location_id'] ?? null;
$locationNotes = '';
if (!$locationId && !empty($locationData)) {
// Create new location for the client
$locData = array_merge($locationData, [
'client_id' => $client->id,
'is_default' => false
]);
$newLocation = $this->clientLocationRepository->create($locData);
$locationId = $newLocation->id;
}
if ($locationId) {
// Fetch location to add details to notes if needed, or just rely on relation.
// For now, let's keep the legacy behavior of adding text to notes for quick reference,
// but also link the ID. Use the provided data or fetch?
// If we have an ID, we might not have the text data in $locationData if it came from search.
// So we only append text notes if we have $locationData (Create mode or if frontend sends it).
}
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,
'location_id' => $locationId,
'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes
]);
$intervention = $this->interventionRepository->create($interventionData);
// Step 5a: Assign practitioners if provided
if (!empty($validated['intervention']['principal_practitioner_id'])) {
$this->interventionPractitionerRepository->createAssignment(
$intervention->id,
(int)$validated['intervention']['principal_practitioner_id'],
'principal'
);
}
if (!empty($validated['intervention']['assistant_practitioner_ids']) && is_array($validated['intervention']['assistant_practitioner_ids'])) {
foreach ($validated['intervention']['assistant_practitioner_ids'] as $assistantId) {
$this->interventionPractitionerRepository->createAssignment(
$intervention->id,
(int)$assistantId,
'assistant'
);
}
}
if (
empty($validated['intervention']['principal_practitioner_id']) &&
!empty($validated['intervention']['practitioners']) &&
is_array($validated['intervention']['practitioners'])
) {
foreach ($validated['intervention']['practitioners'] as $index => $practitionerId) {
$role = $index === 0 ? 'principal' : 'assistant';
$this->interventionPractitionerRepository->createAssignment(
$intervention->id,
(int)$practitionerId,
$role
);
}
}
$intervention->load('practitioners');
// Step 5b: Create a Quote for this intervention
try {
$interventionProduct = $this->productRepository->findInterventionProduct();
if ($interventionProduct) {
// Calculate totals
$quantity = 1;
// Ideally fetch TVA rate from product, default to 20% if not set
// Assuming product has tva_rate relationship or simple logic
$tvaRateValue = 20;
$unitPrice = $interventionProduct->prix_unitaire;
$totalHt = $unitPrice * $quantity;
$totalTva = $totalHt * ($tvaRateValue / 100);
$totalTtc = $totalHt + $totalTva;
$quoteData = [
'client_id' => $client->id,
'status' => 'brouillon',
'quote_date' => now()->toDateString(),
'currency' => 'EUR',
'valid_until' => now()->addDays(30)->toDateString(),
'total_ht' => $totalHt,
'total_tva' => $totalTva,
'total_ttc' => $totalTtc,
'lines' => [
[
'product_id' => $interventionProduct->id,
'description' => 'Intervention: ' . ($intervention->type ?? 'Standard'),
'units_qty' => $quantity,
'unit_price' => $unitPrice,
'discount_pct' => 0,
'total_ht' => $totalHt,
// 'tva_rate_id' => ... if needed
]
]
];
$quote = $this->quoteRepository->create($quoteData);
// Update the intervention with the newly created quote ID
$intervention->update(['quote_id' => $quote->id]);
Log::info('Quote auto-created for intervention', ['intervention_id' => $intervention->id, 'quote_id' => $quote->id]);
}
else {
Log::warning('No intervention product found, skipping auto-quote creation', ['intervention_id' => $intervention->id]);
}
}
catch (\Exception $e) {
Log::error('Failed to auto-create quote for intervention: ' . $e->getMessage());
// Silently fail for the quote part to not block intervention creation
}
// 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,222 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInvoiceRequest;
use App\Http\Requests\UpdateInvoiceRequest;
use App\Http\Resources\InvoiceResource;
use App\Repositories\InvoiceRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Mail;
use App\Mail\DocumentMail;
class InvoiceController extends Controller
{
public function __construct(
protected InvoiceRepositoryInterface $invoiceRepository
) {
}
/**
* Display a listing of invoices.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$invoices = $this->invoiceRepository->all();
return InvoiceResource::collection($invoices);
} catch (\Exception $e) {
Log::error('Error fetching invoices: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des factures.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created invoice.
*/
public function store(StoreInvoiceRequest $request): InvoiceResource|JsonResponse
{
try {
$invoice = $this->invoiceRepository->create($request->validated());
return new InvoiceResource($invoice);
} catch (\Exception $e) {
Log::error('Error creating invoice: ' . $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 facture.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified invoice.
*/
public function show(string $id): InvoiceResource|JsonResponse
{
try {
$invoice = $this->invoiceRepository->find($id);
if (! $invoice) {
return response()->json([
'message' => 'Facture non trouvée.',
], 404);
}
return new InvoiceResource($invoice);
} catch (\Exception $e) {
Log::error('Error fetching invoice: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'invoice_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la facture.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified invoice.
*/
public function update(UpdateInvoiceRequest $request, string $id): InvoiceResource|JsonResponse
{
try {
$updated = $this->invoiceRepository->update($id, $request->validated());
if (! $updated) {
return response()->json([
'message' => 'Facture non trouvée ou échec de la mise à jour.',
], 404);
}
$invoice = $this->invoiceRepository->find($id);
return new InvoiceResource($invoice);
} catch (\Exception $e) {
Log::error('Error updating invoice: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'invoice_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de la facture.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified invoice.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->invoiceRepository->delete($id);
if (! $deleted) {
return response()->json([
'message' => 'Facture non trouvée ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Facture supprimée avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting invoice: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'invoice_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de la facture.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Create an invoice from a quote.
*/
public function createFromQuote(string $quoteId): InvoiceResource|JsonResponse
{
try {
$invoice = $this->invoiceRepository->createFromQuote($quoteId);
return new InvoiceResource($invoice);
} catch (\Exception $e) {
Log::error('Error creating invoice from quote: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'quote_id' => $quoteId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création de la facture depuis le devis.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Send the invoice by email to the client.
*/
public function sendByEmail(string $id): JsonResponse
{
try {
$invoice = $this->invoiceRepository->find($id);
if (!$invoice) {
return response()->json(['message' => 'Facture non trouvée.'], 404);
}
if (!$invoice->client || !$invoice->client->email) {
return response()->json(['message' => 'Le client n\'a pas d\'adresse email.'], 422);
}
// Load lines to ensure they are available in the view
$invoice->load('lines');
// Generate PDF
$pdfContent = Pdf::loadView('pdf.invoice_pdf', ['invoice' => $invoice])->output();
// Send Email
Mail::to($invoice->client->email)->send(new DocumentMail($invoice, 'invoice', $pdfContent));
return response()->json([
'message' => 'La facture a été envoyée avec succès à ' . $invoice->client->email,
], 200);
} catch (\Exception $e) {
Log::error('Error sending invoice email: ' . $e->getMessage(), [
'exception' => $e,
'invoice_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePractitionerDocumentRequest;
use App\Http\Requests\UpdatePractitionerDocumentRequest;
use App\Http\Resources\Employee\PractitionerDocumentResource;
use App\Http\Resources\Employee\PractitionerDocumentCollection;
use App\Repositories\PractitionerDocumentRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class PractitionerDocumentController extends Controller
{
public function __construct(
private readonly PractitionerDocumentRepositoryInterface $practitionerDocumentRepository
) {
}
/**
* Display a listing of practitioner documents.
*/
public function index(Request $request): PractitionerDocumentCollection|JsonResponse
{
try {
$filters = [
'search' => $request->get('search'),
'practitioner_id' => $request->get('practitioner_id'),
'doc_type' => $request->get('doc_type'),
'valid_only' => $request->get('valid_only'),
'sort_by' => $request->get('sort_by', 'created_at'),
'sort_direction' => $request->get('sort_direction', 'desc'),
];
// Remove null filters
$filters = array_filter($filters, function ($value) {
return $value !== null && $value !== '';
});
$documents = $this->practitionerDocumentRepository->getAll($filters);
return new PractitionerDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Error fetching practitioner documents: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents des praticiens.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display paginated practitioner documents.
*/
public function paginated(Request $request): JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$result = $this->practitionerDocumentRepository->getPaginated($perPage);
return response()->json([
'data' => new PractitionerDocumentCollection($result['documents']),
'pagination' => $result['pagination'],
'message' => 'Documents des praticiens récupérés avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error fetching paginated practitioner documents: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents des praticiens.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get documents by practitioner ID.
*/
public function byPractitioner(string $practitionerId): PractitionerDocumentCollection|JsonResponse
{
try {
$documents = $this->practitionerDocumentRepository->getByPractitionerId((int) $practitionerId);
return new PractitionerDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Error fetching documents by practitioner: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'practitioner_id' => $practitionerId,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents du praticien.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get documents by type.
*/
public function byType(Request $request): PractitionerDocumentCollection|JsonResponse
{
try {
$docType = $request->get('doc_type');
if (!$docType) {
return response()->json([
'message' => 'Le paramètre doc_type est requis.',
], 400);
}
$documents = $this->practitionerDocumentRepository->getByDocumentType($docType);
return new PractitionerDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Error fetching documents by type: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'doc_type' => $docType,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents par type.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get valid documents (not expired).
*/
public function valid(): PractitionerDocumentCollection|JsonResponse
{
try {
$documents = $this->practitionerDocumentRepository->getValid();
return new PractitionerDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Error fetching valid documents: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents valides.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get expired documents.
*/
public function expired(): PractitionerDocumentCollection|JsonResponse
{
try {
$documents = $this->practitionerDocumentRepository->getExpired();
return new PractitionerDocumentCollection($documents);
} catch (\Exception $e) {
Log::error('Error fetching expired documents: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des documents expirés.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Get practitioner document statistics.
*/
public function statistics(): JsonResponse
{
try {
$statistics = $this->practitionerDocumentRepository->getStatistics();
return response()->json([
'data' => $statistics,
'message' => 'Statistiques des documents des praticiens récupérées avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error fetching practitioner document statistics: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des statistiques.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created practitioner document.
*/
public function store(StorePractitionerDocumentRequest $request): PractitionerDocumentResource|JsonResponse
{
try {
$document = $this->practitionerDocumentRepository->create($request->validated());
return new PractitionerDocumentResource($document);
} catch (\Exception $e) {
Log::error('Error creating practitioner document: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du document du praticien.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified practitioner document.
*/
public function show(string $id): PractitionerDocumentResource|JsonResponse
{
try {
$document = $this->practitionerDocumentRepository->find($id);
if (!$document) {
return response()->json([
'message' => 'Document du praticien non trouvé.',
], 404);
}
return new PractitionerDocumentResource($document);
} catch (\Exception $e) {
Log::error('Error fetching practitioner document: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'document_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du document du praticien.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified practitioner document.
*/
public function update(UpdatePractitionerDocumentRequest $request, string $id): PractitionerDocumentResource|JsonResponse
{
try {
$updated = $this->practitionerDocumentRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Document du praticien non trouvé ou échec de la mise à jour.',
], 404);
}
$document = $this->practitionerDocumentRepository->find($id);
return new PractitionerDocumentResource($document);
} catch (\Exception $e) {
Log::error('Error updating practitioner document: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'document_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du document du praticien.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified practitioner document.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->practitionerDocumentRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Document du praticien non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Document du praticien supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting practitioner document: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'document_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du document du praticien.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePriceListRequest;
use App\Http\Requests\UpdatePriceListRequest;
use App\Http\Resources\PriceListResource;
use App\Repositories\PriceListRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class PriceListController extends Controller
{
public function __construct(
private readonly PriceListRepositoryInterface $priceListRepository
) {
}
/**
* Display a listing of price lists.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$priceLists = $this->priceListRepository->all()->sortBy('name')->values();
return PriceListResource::collection($priceLists);
} catch (\Exception $e) {
Log::error('Error fetching price lists: ' . $e->getMessage(), [
'exception' => $e,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des listes de prix.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created price list.
*/
public function store(StorePriceListRequest $request): PriceListResource|JsonResponse
{
try {
$priceList = $this->priceListRepository->create($request->validated());
return new PriceListResource($priceList);
} catch (\Exception $e) {
Log::error('Error creating price list: ' . $e->getMessage(), [
'exception' => $e,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création de la liste de prix.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified price list.
*/
public function show(string $id): PriceListResource|JsonResponse
{
try {
$priceList = $this->priceListRepository->find($id);
if (! $priceList) {
return response()->json([
'message' => 'Liste de prix non trouvée.',
], 404);
}
return new PriceListResource($priceList);
} catch (\Exception $e) {
Log::error('Error fetching price list: ' . $e->getMessage(), [
'exception' => $e,
'price_list_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la liste de prix.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified price list.
*/
public function update(UpdatePriceListRequest $request, string $id): PriceListResource|JsonResponse
{
try {
$updated = $this->priceListRepository->update($id, $request->validated());
if (! $updated) {
return response()->json([
'message' => 'Liste de prix non trouvée ou échec de la mise à jour.',
], 404);
}
$priceList = $this->priceListRepository->find($id);
return new PriceListResource($priceList);
} catch (\Exception $e) {
Log::error('Error updating price list: ' . $e->getMessage(), [
'exception' => $e,
'price_list_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de la liste de prix.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified price list.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->priceListRepository->delete($id);
if (! $deleted) {
return response()->json([
'message' => 'Liste de prix non trouvée ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Liste de prix supprimée avec succès.',
]);
} catch (\Exception $e) {
Log::error('Error deleting price list: ' . $e->getMessage(), [
'exception' => $e,
'price_list_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de la liste de prix.',
'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 = (int) $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 = (int) $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,376 @@
<?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 = (int) $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'),
'is_intervention' => $request->get('is_intervention'),
'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 = (int) $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 = (int) $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,226 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePurchaseOrderRequest;
use App\Http\Requests\UpdatePurchaseOrderRequest;
use App\Http\Resources\Fournisseur\PurchaseOrderResource;
use App\Models\GoodsReceipt;
use App\Models\Warehouse;
use App\Repositories\GoodsReceiptRepositoryInterface;
use App\Repositories\PurchaseOrderRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
class PurchaseOrderController extends Controller
{
public function __construct(
protected PurchaseOrderRepositoryInterface $purchaseOrderRepository,
protected GoodsReceiptRepositoryInterface $goodsReceiptRepository
)
{
}
/**
* Display a listing of purchase orders.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$purchaseOrders = $this->purchaseOrderRepository->all();
return PurchaseOrderResource::collection($purchaseOrders);
}
catch (\Exception $e) {
Log::error('Error fetching purchase orders: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des commandes fournisseurs.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created purchase order.
*/
public function store(StorePurchaseOrderRequest $request): PurchaseOrderResource|JsonResponse
{
try {
$purchaseOrder = $this->purchaseOrderRepository->create($request->validated());
// If PO is created directly as validated/delivered, ensure a draft goods receipt exists.
if ($purchaseOrder && in_array($purchaseOrder->status, ['confirmee', 'livree'], true)) {
$this->createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder);
}
return new PurchaseOrderResource($purchaseOrder);
}
catch (\Exception $e) {
Log::error('Error creating purchase order: ' . $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 commande fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified purchase order.
*/
public function show(string $id): PurchaseOrderResource|JsonResponse
{
try {
$purchaseOrder = $this->purchaseOrderRepository->find($id);
if (!$purchaseOrder) {
return response()->json([
'message' => 'Commande fournisseur non trouvée.',
], 404);
}
return new PurchaseOrderResource($purchaseOrder);
}
catch (\Exception $e) {
Log::error('Error fetching purchase order: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'purchase_order_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de la commande fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified purchase order.
*/
public function update(UpdatePurchaseOrderRequest $request, string $id): PurchaseOrderResource|JsonResponse
{
try {
$updated = $this->purchaseOrderRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Commande fournisseur non trouvée ou échec de la mise à jour.',
], 404);
}
$purchaseOrder = $this->purchaseOrderRepository->find($id);
// Ensure draft goods receipt exists when PO is validated/delivered.
// Idempotent: guarded by purchase_order_id existence check in helper.
if ($purchaseOrder && in_array($purchaseOrder->status, ['confirmee', 'livree'], true)) {
$this->createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder);
}
return new PurchaseOrderResource($purchaseOrder);
}
catch (\Exception $e) {
Log::error('Error updating purchase order: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'purchase_order_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de la commande fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Create a draft goods receipt when a purchase order is validated.
*/
protected function createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder): void
{
$alreadyExists = GoodsReceipt::query()
->where('purchase_order_id', $purchaseOrder->id)
->exists();
if ($alreadyExists) {
return;
}
$warehouseId = Warehouse::query()->value('id');
if (!$warehouseId) {
throw new \RuntimeException('Aucun entrepôt disponible pour créer la réception de marchandise.');
}
$receiptNumber = 'GR-' . now()->format('Ym') . '-' . str_pad((string)$purchaseOrder->id, 4, '0', STR_PAD_LEFT);
$lines = collect($purchaseOrder->lines ?? [])
->filter(fn($line) => !empty($line->product_id))
->map(function ($line) {
return [
'product_id' => (int)$line->product_id,
'packaging_id' => null,
'packages_qty_received' => null,
'units_qty_received' => (float)$line->quantity,
'qty_received_base' => (float)$line->quantity,
'unit_price' => (float)$line->unit_price,
'unit_price_per_package' => null,
'tva_rate_id' => null,
];
})
->values()
->all();
$this->goodsReceiptRepository->create([
'purchase_order_id' => $purchaseOrder->id,
'warehouse_id' => (int)$warehouseId,
'receipt_number' => $receiptNumber,
'receipt_date' => now()->toDateString(),
'status' => 'draft',
'lines' => $lines,
]);
}
/**
* Remove the specified purchase order.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->purchaseOrderRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Commande fournisseur non trouvée ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Commande fournisseur supprimée avec succès.',
], 200);
}
catch (\Exception $e) {
Log::error('Error deleting purchase order: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'purchase_order_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de la commande fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreQuoteRequest;
use App\Http\Requests\UpdateQuoteRequest;
use App\Http\Resources\QuoteResource;
use App\Models\Quote;
use App\Repositories\QuoteRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Mail;
use App\Mail\DocumentMail;
use Symfony\Component\HttpFoundation\Response;
class QuoteController extends Controller
{
public function __construct(
protected QuoteRepositoryInterface $quoteRepository
) {
}
/**
* Display a listing of quotes.
*/
public function index(): AnonymousResourceCollection|JsonResponse
{
try {
$quotes = $this->quoteRepository->all();
return QuoteResource::collection($quotes);
} catch (\Exception $e) {
Log::error('Error fetching quotes: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des devis.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created quote.
*/
public function store(StoreQuoteRequest $request): QuoteResource|JsonResponse
{
try {
$quote = $this->quoteRepository->create($request->validated());
return new QuoteResource($quote);
} catch (\Exception $e) {
Log::error('Error creating quote: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du devis.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified quote.
*/
public function show(string $id): QuoteResource|JsonResponse
{
try {
$quote = $this->quoteRepository->find($id);
if (! $quote) {
return response()->json([
'message' => 'Devis non trouvé.',
], 404);
}
return new QuoteResource($quote);
} catch (\Exception $e) {
Log::error('Error fetching quote: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'quote_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du devis.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified quote.
*/
public function update(UpdateQuoteRequest $request, string $id): QuoteResource|JsonResponse
{
try {
$updated = $this->quoteRepository->update($id, $request->validated());
if (! $updated) {
return response()->json([
'message' => 'Devis non trouvé ou échec de la mise à jour.',
], 404);
}
$quote = $this->quoteRepository->find($id);
return new QuoteResource($quote);
} catch (\Exception $e) {
Log::error('Error updating quote: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'quote_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du devis.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified quote.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->quoteRepository->delete($id);
if (! $deleted) {
return response()->json([
'message' => 'Devis non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Devis supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting quote: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'quote_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du devis.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Send the quote by email to the client.
*/
public function sendByEmail(string $id): JsonResponse
{
try {
$quote = $this->quoteRepository->find($id);
if (!$quote) {
return response()->json(['message' => 'Devis non trouvé.'], 404);
}
if (!$quote->client || !$quote->client->email) {
return response()->json(['message' => 'Le client n\'a pas d\'adresse email.'], 422);
}
// Load lines to ensure they are available in the view
$quote->load('lines');
// Generate PDF
$pdfContent = Pdf::loadView('pdf.quote_pdf', ['quote' => $quote])->output();
// Send Email
Mail::to($quote->client->email)->send(new DocumentMail($quote, 'quote', $pdfContent));
return response()->json([
'message' => 'Le devis a été envoyé avec succès à ' . $quote->client->email,
], 200);
} catch (\Exception $e) {
Log::error('Error sending quote email: ' . $e->getMessage(), [
'exception' => $e,
'quote_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Download the quote as a PDF.
*/
public function downloadPdf(string $id): Response|JsonResponse
{
try {
$quote = Quote::with(['client', 'group', 'lines'])->find($id);
if (! $quote) {
return response()->json([
'message' => 'Devis non trouvé.',
], 404);
}
$pdf = Pdf::loadView('pdf.quote_pdf', ['quote' => $quote]);
return $pdf->download('Devis_' . $quote->reference . '.pdf');
} catch (\Exception $e) {
Log::error('Error downloading quote PDF: ' . $e->getMessage(), [
'exception' => $e,
'quote_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la generation du PDF.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreStockItemRequest;
use App\Http\Requests\UpdateStockItemRequest;
use App\Http\Resources\StockItemResource;
use App\Repositories\StockItemRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class StockItemController extends Controller
{
public function __construct(
private readonly StockItemRepositoryInterface $stockItemRepository
) {
}
/**
* Display a listing of stock items.
*/
public function index(): JsonResponse
{
try {
$items = $this->stockItemRepository->all();
return response()->json([
'data' => StockItemResource::collection($items),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching stock items: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des stocks.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created stock item.
*/
public function store(StoreStockItemRequest $request): JsonResponse
{
try {
$item = $this->stockItemRepository->create($request->validated());
return response()->json([
'data' => new StockItemResource($item),
'message' => 'Stock initialisé avec succès.',
'status' => 'success'
], 201);
} catch (\Exception $e) {
Log::error('Error creating stock item: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de l\'initialisation du stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified stock item.
*/
public function show(string $id): JsonResponse
{
try {
$item = $this->stockItemRepository->find((int) $id);
if (!$item) {
return response()->json(['message' => 'Stock non trouvé.'], 404);
}
return response()->json([
'data' => new StockItemResource($item),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching stock item: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified stock item.
*/
public function update(UpdateStockItemRequest $request, string $id): JsonResponse
{
try {
$updated = $this->stockItemRepository->update((int) $id, $request->validated());
if (!$updated) {
return response()->json(['message' => 'Stock non trouvé ou échec de la mise à jour.'], 404);
}
$item = $this->stockItemRepository->find((int) $id);
return response()->json([
'data' => new StockItemResource($item),
'message' => 'Stock mis à jour avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error updating stock item: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified stock item.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->stockItemRepository->delete((int) $id);
if (!$deleted) {
return response()->json(['message' => 'Stock non trouvé ou échec de la suppression.'], 404);
}
return response()->json([
'message' => 'Stock supprimé avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error deleting stock item: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreStockMoveRequest;
use App\Http\Resources\StockMoveResource;
use App\Repositories\StockMoveRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class StockMoveController extends Controller
{
public function __construct(
private readonly StockMoveRepositoryInterface $stockMoveRepository
) {
}
/**
* Display a listing of stock moves.
*/
public function index(Request $request): JsonResponse
{
try {
$moves = $this->stockMoveRepository->all();
return response()->json([
'data' => StockMoveResource::collection($moves),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching stock moves: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des mouvements de stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created stock move.
*/
public function store(StoreStockMoveRequest $request): JsonResponse
{
try {
$move = $this->stockMoveRepository->create($request->validated());
return response()->json([
'data' => new StockMoveResource($move),
'message' => 'Mouvement de stock enregistré avec succès.',
'status' => 'success'
], 201);
} catch (\Exception $e) {
Log::error('Error creating stock move: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de l\'enregistrement du mouvement de stock.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified stock move.
*/
public function show(string $id): JsonResponse
{
try {
$move = $this->stockMoveRepository->find((int) $id);
if (!$move) {
return response()->json(['message' => 'Mouvement de stock non trouvé.'], 404);
}
return response()->json([
'data' => new StockMoveResource($move),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching stock move: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du mouvement de 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,133 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreTvaRateRequest;
use App\Http\Requests\UpdateTvaRateRequest;
use App\Http\Resources\TvaRateResource;
use App\Repositories\TvaRateRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class TvaRateController extends Controller
{
public function __construct(
private readonly TvaRateRepositoryInterface $tvaRateRepository
) {
}
/**
* Display a listing of TVA rates.
*/
public function index(): JsonResponse
{
try {
$tvaRates = $this->tvaRateRepository->all();
return response()->json([
'data' => TvaRateResource::collection($tvaRates),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching TVA rates: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des taux de TVA.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created TVA rate.
*/
public function store(StoreTvaRateRequest $request): JsonResponse
{
try {
$tvaRate = $this->tvaRateRepository->create($request->validated());
return response()->json([
'data' => new TvaRateResource($tvaRate),
'message' => 'Taux de TVA créé avec succès.',
'status' => 'success'
], 201);
} catch (\Exception $e) {
Log::error('Error creating TVA rate: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la création du taux de TVA.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified TVA rate.
*/
public function show(string $id): JsonResponse
{
try {
$tvaRate = $this->tvaRateRepository->find((int) $id);
if (!$tvaRate) {
return response()->json(['message' => 'Taux de TVA non trouvé.'], 404);
}
return response()->json([
'data' => new TvaRateResource($tvaRate),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching TVA rate: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du taux de TVA.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified TVA rate.
*/
public function update(UpdateTvaRateRequest $request, string $id): JsonResponse
{
try {
$updated = $this->tvaRateRepository->update((int) $id, $request->validated());
if (!$updated) {
return response()->json(['message' => 'Taux de TVA non trouvé ou échec de la mise à jour.'], 404);
}
$tvaRate = $this->tvaRateRepository->find((int) $id);
return response()->json([
'data' => new TvaRateResource($tvaRate),
'message' => 'Taux de TVA mis à jour avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error updating TVA rate: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du taux de TVA.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified TVA rate.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->tvaRateRepository->delete((int) $id);
if (!$deleted) {
return response()->json(['message' => 'Taux de TVA non trouvé ou échec de la suppression.'], 404);
}
return response()->json([
'message' => 'Taux de TVA supprimé avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error deleting TVA rate: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du taux de TVA.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreWarehouseRequest;
use App\Http\Requests\UpdateWarehouseRequest;
use App\Http\Resources\WarehouseResource;
use App\Repositories\WarehouseRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class WarehouseController extends Controller
{
public function __construct(
private readonly WarehouseRepositoryInterface $warehouseRepository
) {
}
/**
* Display a listing of warehouses.
*/
public function index(): JsonResponse
{
try {
$warehouses = $this->warehouseRepository->all();
return response()->json([
'data' => WarehouseResource::collection($warehouses),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching warehouses: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des entrepôts.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created warehouse.
*/
public function store(StoreWarehouseRequest $request): JsonResponse
{
try {
$warehouse = $this->warehouseRepository->create($request->validated());
return response()->json([
'data' => new WarehouseResource($warehouse),
'message' => 'Entrepôt créé avec succès.',
'status' => 'success'
], 201);
} catch (\Exception $e) {
Log::error('Error creating warehouse: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la création de l\'entrepôt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display specified warehouse.
*/
public function show(string $id): JsonResponse
{
try {
$warehouse = $this->warehouseRepository->find((int) $id);
if (!$warehouse) {
return response()->json(['message' => 'Entrepôt non trouvé.'], 404);
}
return response()->json([
'data' => new WarehouseResource($warehouse),
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error fetching warehouse: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération de l\'entrepôt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update specified warehouse.
*/
public function update(UpdateWarehouseRequest $request, string $id): JsonResponse
{
try {
$updated = $this->warehouseRepository->update((int) $id, $request->validated());
if (!$updated) {
return response()->json(['message' => 'Entrepôt non trouvé ou échec de la mise à jour.'], 404);
}
$warehouse = $this->warehouseRepository->find((int) $id);
return response()->json([
'data' => new WarehouseResource($warehouse),
'message' => 'Entrepôt mis à jour avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error updating warehouse: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour de l\'entrepôt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove specified warehouse.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->warehouseRepository->delete((int) $id);
if (!$deleted) {
return response()->json(['message' => 'Entrepôt non trouvé ou échec de la suppression.'], 404);
}
return response()->json([
'message' => 'Entrepôt supprimé avec succès.',
'status' => 'success'
]);
} catch (\Exception $e) {
Log::error('Error deleting warehouse: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression de l\'entrepôt.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Search warehouses by name.
*/
public function searchBy(Request $request): JsonResponse
{
try {
$name = $request->query('name');
$exactMatch = $request->query('exact_match', false);
if (empty($name)) {
return response()->json([
'data' => [],
'count' => 0,
'message' => 'Le paramètre de recherche est requis.'
], 400);
}
$warehouses = $this->warehouseRepository->all();
$filtered = $warehouses->filter(function ($warehouse) use ($name, $exactMatch) {
if ($exactMatch) {
return strtolower($warehouse->name) === strtolower($name);
}
return stripos($warehouse->name, $name) !== false;
});
return response()->json([
'data' => WarehouseResource::collection($filtered),
'count' => $filtered->count(),
'message' => 'Recherche effectuée avec succès.'
]);
} catch (\Exception $e) {
Log::error('Error searching warehouses: ' . $e->getMessage());
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des entrepôts.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AssignClientsToGroupRequest 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_ids' => 'required|array|min:1',
'client_ids.*' => 'integer|distinct|exists:clients,id',
];
}
public function messages(): array
{
return [
'client_ids.required' => 'La liste des clients est obligatoire.',
'client_ids.array' => 'La liste des clients doit être un tableau.',
'client_ids.min' => 'Veuillez sélectionner au moins un client.',
'client_ids.*.integer' => 'Chaque ID client doit être un entier.',
'client_ids.*.distinct' => 'Un client ne peut pas être envoyé plusieurs fois.',
'client_ids.*.exists' => 'Un ou plusieurs clients sélectionnés sont introuvables.',
];
}
}

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,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreAvoirRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'client_id' => 'required|exists:clients,id',
'invoice_id' => 'nullable|exists:invoices,id',
'group_id' => 'nullable|exists:client_groups,id',
'status' => 'required|in:brouillon,emis,applique,annule',
'avoir_date' => 'required|date',
'due_date' => 'nullable|date|after_or_equal:avoir_date',
'currency' => 'required|string|size:3',
'total_ht' => 'required|numeric',
'total_tva' => 'required|numeric',
'total_ttc' => 'required|numeric',
'reason_type' => 'required|in:remboursement_total,remboursement_partiel,reduction,erreur_facturation,retour_marchandise,accord_commercial,autre',
'reason_description' => 'nullable|string',
'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive',
'refund_status' => 'nullable|in:non_rembourse,en_cours,partiellement_rembourse,rembourse,compense',
'refund_date' => 'nullable|date',
'refund_method' => 'nullable|in:virement,cheque,carte_credit,compensation_future,autre',
'compensation_invoice_id' => 'nullable|exists:invoices,id',
'compensation_amount' => 'nullable|numeric|min:0',
'lines' => 'required|array|min:1',
'lines.*.product_id' => 'nullable|exists:products,id',
'lines.*.invoice_line_id' => 'nullable|exists:invoice_lines,id',
'lines.*.description' => 'required|string',
'lines.*.quantity' => 'required|numeric',
'lines.*.unit_price' => 'required|numeric',
'lines.*.tva_rate' => 'required|numeric',
'lines.*.total_ht' => 'required|numeric',
'lines.*.total_tva' => 'required|numeric',
'lines.*.total_ttc' => 'required|numeric',
'lines.*.notes' => 'nullable|string',
];
}
}

View File

@ -0,0 +1,46 @@
<?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',
'client_ids' => 'sometimes|array',
'client_ids.*' => 'integer|distinct|exists:clients,id',
];
}
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.',
'client_ids.array' => 'La liste des clients doit être un tableau.',
'client_ids.*.integer' => 'Chaque ID client doit être un entier.',
'client_ids.*.distinct' => 'Un client ne peut pas être sélectionné plusieurs fois.',
'client_ids.*.exists' => 'Un ou plusieurs clients sélectionnés sont introuvables.',
];
}
}

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,72 @@
<?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' => 'required|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',
'is_parent' => 'boolean|nullable',
'parent_id' => 'nullable|exists:clients,id',
'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.required' => 'L\'adresse facturation est obligatoire.',
'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,79 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreGoodsReceiptRequest 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 [
'purchase_order_id' => 'required|exists:purchase_orders,id',
'warehouse_id' => 'required|exists:warehouses,id',
'receipt_number' => 'required|string|max:191',
'receipt_date' => 'required|date',
'status' => 'nullable|in:draft,posted',
'notes' => 'nullable|string',
'lines' => 'nullable|array',
'lines.*.product_id' => 'required_with:lines|exists:products,id',
'lines.*.packaging_id' => 'nullable|exists:product_packagings,id',
'lines.*.packages_qty_received' => 'nullable|numeric|min:0',
'lines.*.units_qty_received' => 'nullable|numeric|min:0',
'lines.*.qty_received_base' => 'nullable|numeric|min:0',
'lines.*.unit_price' => 'nullable|numeric|min:0',
'lines.*.unit_price_per_package' => 'nullable|numeric|min:0',
'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'purchase_order_id.required' => 'La commande fournisseur est requise.',
'purchase_order_id.exists' => 'La commande fournisseur spécifiée n\'existe pas.',
'warehouse_id.required' => 'L\'entrepôt est requis.',
'warehouse_id.exists' => 'L\'entrepôt spécifié n\'existe pas.',
'receipt_number.required' => 'Le numéro de réception est requis.',
'receipt_number.string' => 'Le numéro de réception doit être une chaîne de caractères.',
'receipt_number.max' => 'Le numéro de réception ne peut pas dépasser 191 caractères.',
'receipt_date.required' => 'La date de réception est requise.',
'receipt_date.date' => 'La date de réception doit être une date valide.',
'status.in' => 'Le statut doit être "draft" ou "posted".',
'notes.string' => 'Les notes doivent être une chaîne de caractères.',
'lines.array' => 'Les lignes doivent être un tableau.',
'lines.*.product_id.required_with' => 'Le produit est requis pour chaque ligne.',
'lines.*.product_id.exists' => 'Le produit spécifié dans une ligne n\'existe pas.',
'lines.*.packaging_id.exists' => 'Le conditionnement spécifié dans une ligne n\'existe pas.',
'lines.*.packages_qty_received.numeric' => 'La quantité de colis doit être un nombre.',
'lines.*.packages_qty_received.min' => 'La quantité de colis ne peut pas être négative.',
'lines.*.units_qty_received.numeric' => 'La quantité d\'unités doit être un nombre.',
'lines.*.units_qty_received.min' => 'La quantité d\'unités ne peut pas être négative.',
'lines.*.qty_received_base.numeric' => 'La quantité de base doit être un nombre.',
'lines.*.qty_received_base.min' => 'La quantité de base ne peut pas être négative.',
'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.',
'lines.*.unit_price.min' => 'Le prix unitaire ne peut pas être négatif.',
'lines.*.unit_price_per_package.numeric' => 'Le prix par colis doit être un nombre.',
'lines.*.unit_price_per_package.min' => 'Le prix par colis ne peut pas être négatif.',
'lines.*.tva_rate_id.exists' => 'Le taux de TVA spécifié dans une ligne n\'existe pas.',
];
}
}

View File

@ -0,0 +1,84 @@
<?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'],
'product_id' => ['nullable', 'exists:products,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,205 @@
<?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_id' => ['nullable', 'exists:deceased,id'],
'deceased' => ['required_without:deceased_id', 'array'],
'deceased.last_name' => ['required_without:deceased_id', '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_id' => ['nullable', 'exists:clients,id'],
'client' => 'required_without:client_id|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_id' => ['nullable', 'exists:client_locations,id'],
'location' => ['nullable', 'array'],
'location.name' => ['required_without:location_id', 'string', 'max:255'],
'location.address' => ['nullable', 'string', 'max:255'],
'location.city' => ['required_without:location_id', '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,99 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreInvoiceRequest 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',
'group_id' => 'nullable|exists:client_groups,id',
'source_quote_id' => 'nullable|exists:quotes,id',
'status' => 'required|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir',
'invoice_date' => 'required|date',
'due_date' => 'nullable|date|after_or_equal:invoice_date',
'currency' => 'required|string|size:3',
'total_ht' => 'required|numeric|min:0',
'total_tva' => 'required|numeric|min:0',
'total_ttc' => 'required|numeric|min:0',
'e_invoicing_channel_id' => 'nullable|exists:e_invoicing_channels,id',
'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive',
'lines' => 'required|array|min:1',
'lines.*.product_id' => 'nullable|exists:products,id',
'lines.*.packaging_id' => 'nullable|exists:product_packagings,id',
'lines.*.packages_qty' => 'nullable|numeric|min:0',
'lines.*.units_qty' => 'nullable|numeric|min:0',
'lines.*.description' => 'required|string',
'lines.*.qty_base' => 'nullable|numeric|min:0',
'lines.*.unit_price' => 'required|numeric|min:0',
'lines.*.unit_price_per_package' => 'nullable|numeric|min:0',
'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id',
'lines.*.discount_pct' => 'required|numeric|min:0|max:100',
'lines.*.total_ht' => 'required|numeric|min:0',
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$hasClient = filled($this->input('client_id'));
$hasGroup = filled($this->input('group_id'));
if (! $hasClient && ! $hasGroup) {
$message = 'Un client ou un groupe de clients est obligatoire.';
$validator->errors()->add('client_id', $message);
$validator->errors()->add('group_id', $message);
}
if ($hasClient && $hasGroup) {
$message = 'Selectionnez soit un client, soit un groupe de clients.';
$validator->errors()->add('client_id', $message);
$validator->errors()->add('group_id', $message);
}
});
}
public function messages(): array
{
return [
'client_id.exists' => 'Le client sélectionné est invalide.',
'group_id.exists' => 'Le groupe sélectionné est invalide.',
'status.required' => 'Le statut est obligatoire.',
'status.in' => 'Le statut sélectionné est invalide.',
'invoice_date.required' => 'La date de la facture est obligatoire.',
'invoice_date.date' => 'La date de la facture n\'est pas valide.',
'due_date.date' => 'La date d\'échéance n\'est pas valide.',
'due_date.after_or_equal' => 'La date d\'échéance doit être postérieure ou égale à la date de la facture.',
'currency.required' => 'La devise est obligatoire.',
'currency.size' => 'La devise doit comporter 3 caractères.',
'total_ht.required' => 'Le total HT est obligatoire.',
'total_ht.numeric' => 'Le total HT doit être un nombre.',
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
'total_tva.required' => 'Le total TVA est obligatoire.',
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
'total_ttc.required' => 'Le total TTC est obligatoire.',
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
'lines.required' => 'Veuillez ajouter au moins une ligne à la facture.',
];
}
}

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,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePriceListRequest 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:price_lists,name',
'valid_from' => 'nullable|date',
'valid_to' => 'nullable|date|after_or_equal:valid_from',
'is_default' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom de la liste de prix est obligatoire.',
'name.unique' => 'Une liste de prix avec ce nom existe déjà.',
'valid_from.date' => 'La date de début doit être une date valide.',
'valid_to.date' => 'La date de fin doit être une date valide.',
'valid_to.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.',
'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,33 @@
<?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 [
'code' => 'required|string|max:64|unique:product_categories,code',
'name' => 'required|string|max:191',
'description' => 'nullable|string',
'parent_id' => 'nullable|exists:product_categories,id',
'intervention' => 'boolean',
'active' => 'boolean',
];
}
}

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,80 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePurchaseOrderRequest 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 [
'fournisseur_id' => ['required', 'exists:fournisseurs,id'],
'po_number' => ['nullable', 'string', 'max:191', 'unique:purchase_orders,po_number'],
'status' => ['nullable', 'in:brouillon,confirmee,livree,facturee,annulee'],
'order_date' => ['nullable', 'date'],
'expected_date' => ['nullable', 'date'],
'currency' => ['nullable', 'string', 'size:3'],
'total_ht' => ['required', 'numeric', 'min:0'],
'total_tva' => ['required', 'numeric', 'min:0'],
'total_ttc' => ['required', 'numeric', 'min:0'],
'notes' => ['nullable', 'string'],
'delivery_address' => ['nullable', 'string'],
'lines' => ['required', 'array', 'min:1'],
'lines.*.product_id' => ['nullable', 'exists:products,id'],
'lines.*.description' => ['required', 'string'],
'lines.*.quantity' => ['required', 'numeric', 'min:0.001'],
'lines.*.unit_price' => ['required', 'numeric', 'min:0'],
'lines.*.tva_rate' => ['required', 'numeric', 'min:0'],
'lines.*.discount_pct' => ['nullable', 'numeric', 'min:0', 'max:100'],
'lines.*.total_ht' => ['required', 'numeric', 'min:0'],
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'fournisseur_id.required' => 'Le fournisseur est obligatoire.',
'fournisseur_id.exists' => 'Le fournisseur sélectionné est invalide.',
'po_number.unique' => 'Ce numéro de commande existe déjà.',
'order_date.date' => 'La date de commande n\'est pas valide.',
'expected_date.date' => 'La date de livraison prévue n\'est pas valide.',
'status.in' => 'Le statut sélectionné est invalide.',
'total_ht.required' => 'Le total HT est obligatoire.',
'total_tva.required' => 'Le total TVA est obligatoire.',
'total_ttc.required' => 'Le total TTC est obligatoire.',
'lines.required' => 'Au moins une ligne d\'article est requise.',
'lines.array' => 'Les lignes doivent être un tableau.',
'lines.min' => 'Vous devez ajouter au moins une ligne d\'article.',
'lines.*.description.required' => 'La désignation est obligatoire pour toutes les lignes.',
'lines.*.quantity.required' => 'La quantité est obligatoire.',
'lines.*.quantity.numeric' => 'La quantité doit être un nombre.',
'lines.*.quantity.min' => 'La quantité doit être supérieure à 0.',
'lines.*.unit_price.required' => 'Le prix unitaire est obligatoire.',
'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.',
'lines.*.unit_price.min' => 'Le prix unitaire doit être positif.',
'lines.*.tva_rate.required' => 'Le taux de TVA est obligatoire.',
'lines.*.total_ht.required' => 'Le total HT de la ligne est obligatoire.',
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreQuoteLineRequest 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 [
'quote_id' => 'required|exists:quotes,id',
'product_id' => 'nullable|exists:products,id',
'packaging_id' => 'nullable|exists:product_packagings,id',
'packages_qty' => 'nullable|numeric|min:0',
'units_qty' => 'nullable|numeric|min:0',
'description' => 'required|string',
'qty_base' => 'nullable|numeric|min:0',
'unit_price' => 'required|numeric|min:0',
'unit_price_per_package' => 'nullable|numeric|min:0',
'tva_rate_id' => 'nullable|exists:tva_rates,id',
'discount_pct' => 'required|numeric|min:0|max:100',
'total_ht' => 'required|numeric|min:0',
];
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreQuoteRequest 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',
'group_id' => 'nullable|exists:client_groups,id',
'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule',
'quote_date' => 'required|date',
'valid_until' => 'nullable|date|after_or_equal:quote_date',
'currency' => 'required|string|size:3',
'total_ht' => 'required|numeric|min:0',
'total_tva' => 'required|numeric|min:0',
'total_ttc' => 'required|numeric|min:0',
'lines' => 'required|array|min:1',
'lines.*.product_id' => 'nullable|exists:products,id',
'lines.*.packaging_id' => 'nullable|exists:product_packagings,id',
'lines.*.packages_qty' => 'nullable|numeric|min:0',
'lines.*.units_qty' => 'nullable|numeric|min:0',
'lines.*.description' => 'required|string',
'lines.*.qty_base' => 'nullable|numeric|min:0',
'lines.*.unit_price' => 'required|numeric|min:0',
'lines.*.unit_price_per_package' => 'nullable|numeric|min:0',
'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id',
'lines.*.discount_pct' => 'required|numeric|min:0|max:100',
'lines.*.total_ht' => 'required|numeric|min:0',
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$hasClient = filled($this->input('client_id'));
$hasGroup = filled($this->input('group_id'));
if (! $hasClient && ! $hasGroup) {
$message = 'Un client ou un groupe de clients est obligatoire.';
$validator->errors()->add('client_id', $message);
$validator->errors()->add('group_id', $message);
}
if ($hasClient && $hasGroup) {
$message = 'Sélectionnez soit un client, soit un groupe de clients.';
$validator->errors()->add('client_id', $message);
$validator->errors()->add('group_id', $message);
}
});
}
public function messages(): array
{
return [
'client_id.nullable' => 'Le client sélectionné est invalide.',
'client_id.exists' => 'Le client sélectionné est invalide.',
'group_id.exists' => 'Le groupe sélectionné est invalide.',
'status.required' => 'Le statut est obligatoire.',
'status.in' => 'Le statut sélectionné est invalide.',
'quote_date.required' => 'La date du devis est obligatoire.',
'quote_date.date' => 'La date du devis n\'est pas valide.',
'valid_until.date' => 'La date de validité n\'est pas valide.',
'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.',
'currency.required' => 'La devise est obligatoire.',
'currency.size' => 'La devise doit comporter 3 caractères.',
'total_ht.required' => 'Le total HT est obligatoire.',
'total_ht.numeric' => 'Le total HT doit être un nombre.',
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
'total_tva.required' => 'Le total TVA est obligatoire.',
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
'total_ttc.required' => 'Le total TTC est obligatoire.',
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreStockItemRequest 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 [
'product_id' => 'required|exists:products,id',
'warehouse_id' => 'required|exists:warehouses,id',
'qty_on_hand_base' => 'nullable|numeric|min:0',
'safety_stock_base' => 'nullable|numeric|min:0',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'product_id.required' => 'Le produit est requis.',
'product_id.exists' => 'Le produit sélectionné est invalide.',
'warehouse_id.required' => 'L\'entrepôt est requis.',
'warehouse_id.exists' => 'L\'entrepôt sélectionné est invalide.',
'qty_on_hand_base.numeric' => 'La quantité en stock doit être un nombre.',
'safety_stock_base.numeric' => 'Le stock de sécurité doit être un nombre.',
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreStockMoveRequest 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 [
'product_id' => 'required|exists:products,id',
'from_warehouse_id' => 'nullable|exists:warehouses,id',
'to_warehouse_id' => 'nullable|exists:warehouses,id',
'packaging_id' => 'nullable|exists:product_packagings,id',
'packages_qty' => 'nullable|numeric|min:0',
'units_qty' => 'nullable|numeric|min:0',
'qty_base' => 'required|numeric',
'move_type' => 'required|string|max:64',
'ref_type' => 'nullable|string|max:64',
'ref_id' => 'nullable|integer',
'moved_at' => 'nullable|date',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'product_id.required' => 'Le produit est requis.',
'product_id.exists' => 'Le produit sélectionné est invalide.',
'from_warehouse_id.exists' => 'L\'entrepôt de départ est invalide.',
'to_warehouse_id.exists' => 'L\'entrepôt d\'arrivée est invalide.',
'packaging_id.exists' => 'Le conditionnement sélectionné est invalide.',
'qty_base.required' => 'La quantité de base est requise.',
'qty_base.numeric' => 'La quantité de base doit être un nombre.',
'move_type.required' => 'Le type de mouvement est requis.',
'moved_at.date' => 'La date du mouvement n\'est pas valide.',
];
}
}

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,47 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreTvaRateRequest 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:50',
'rate' => 'required|numeric|min:0|max:999.99',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.required' => 'Le nom du taux de TVA est requis.',
'name.string' => 'Le nom doit être une chaîne de caractères.',
'name.max' => 'Le nom ne peut pas dépasser 50 caractères.',
'rate.required' => 'Le taux de TVA est requis.',
'rate.numeric' => 'Le taux doit être un nombre.',
'rate.min' => 'Le taux ne peut pas être négatif.',
'rate.max' => 'Le taux ne peut pas dépasser 999.99.',
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreWarehouseRequest 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',
'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',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.required' => 'Le nom de l\'entrepôt est requis.',
'name.string' => 'Le nom doit être une chaîne de caractères.',
'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 comporter exactement 2 caractères.',
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateAvoirRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'status' => 'sometimes|required|in:brouillon,emis,applique,annule',
'avoir_date' => 'sometimes|required|date',
'due_date' => 'nullable|date|after_or_equal:avoir_date',
'refund_status' => 'nullable|in:non_rembourse,en_cours,partiellement_rembourse,rembourse,compense',
'refund_date' => 'nullable|date',
'refund_method' => 'nullable|in:virement,cheque,carte_credit,compensation_future,autre',
'reason_description' => 'nullable|string',
'notes' => 'nullable|string',
];
}
}

View File

@ -0,0 +1,57 @@
<?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
{
$routeClientGroup = $this->route('client_group');
$clientGroupId = is_object($routeClientGroup)
? $routeClientGroup->id
: ($routeClientGroup ?? $this->route('id'));
return [
'name' => [
'required',
'string',
'max:191',
Rule::unique('client_groups', 'name')->ignore($clientGroupId)
],
'description' => 'nullable|string',
'client_ids' => 'sometimes|array',
'client_ids.*' => 'integer|distinct|exists:clients,id',
];
}
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.',
'client_ids.array' => 'La liste des clients doit être un tableau.',
'client_ids.*.integer' => 'Chaque ID client doit être un entier.',
'client_ids.*.distinct' => 'Un client ne peut pas être sélectionné plusieurs fois.',
'client_ids.*.exists' => 'Un ou plusieurs clients sélectionnés sont introuvables.',
];
}
}

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,71 @@
<?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',
'is_parent' => 'boolean|nullable',
'parent_id' => 'nullable|exists:clients,id',
'default_tva_rate_id' => 'nullable|exists:tva_rates,id',
'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
];
}
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,75 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateGoodsReceiptRequest 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 [
'purchase_order_id' => 'sometimes|exists:purchase_orders,id',
'warehouse_id' => 'sometimes|exists:warehouses,id',
'receipt_number' => 'sometimes|string|max:191',
'receipt_date' => 'sometimes|date',
'status' => 'nullable|in:draft,posted',
'notes' => 'nullable|string',
'lines' => 'nullable|array',
'lines.*.product_id' => 'required_with:lines|exists:products,id',
'lines.*.packaging_id' => 'nullable|exists:product_packagings,id',
'lines.*.packages_qty_received' => 'nullable|numeric|min:0',
'lines.*.units_qty_received' => 'nullable|numeric|min:0',
'lines.*.qty_received_base' => 'nullable|numeric|min:0',
'lines.*.unit_price' => 'nullable|numeric|min:0',
'lines.*.unit_price_per_package' => 'nullable|numeric|min:0',
'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'purchase_order_id.exists' => 'La commande fournisseur spécifiée n\'existe pas.',
'warehouse_id.exists' => 'L\'entrepôt spécifié n\'existe pas.',
'receipt_number.string' => 'Le numéro de réception doit être une chaîne de caractères.',
'receipt_number.max' => 'Le numéro de réception ne peut pas dépasser 191 caractères.',
'receipt_date.date' => 'La date de réception doit être une date valide.',
'status.in' => 'Le statut doit être "draft" ou "posted".',
'notes.string' => 'Les notes doivent être une chaîne de caractères.',
'lines.array' => 'Les lignes doivent être un tableau.',
'lines.*.product_id.required_with' => 'Le produit est requis pour chaque ligne.',
'lines.*.product_id.exists' => 'Le produit spécifié dans une ligne n\'existe pas.',
'lines.*.packaging_id.exists' => 'Le conditionnement spécifié dans une ligne n\'existe pas.',
'lines.*.packages_qty_received.numeric' => 'La quantité de colis doit être un nombre.',
'lines.*.packages_qty_received.min' => 'La quantité de colis ne peut pas être négative.',
'lines.*.units_qty_received.numeric' => 'La quantité d\'unités doit être un nombre.',
'lines.*.units_qty_received.min' => 'La quantité d\'unités ne peut pas être négative.',
'lines.*.qty_received_base.numeric' => 'La quantité de base doit être un nombre.',
'lines.*.qty_received_base.min' => 'La quantité de base ne peut pas être négative.',
'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.',
'lines.*.unit_price.min' => 'Le prix unitaire ne peut pas être négatif.',
'lines.*.unit_price_per_package.numeric' => 'Le prix par colis doit être un nombre.',
'lines.*.unit_price_per_package.min' => 'Le prix par colis ne peut pas être négatif.',
'lines.*.tva_rate_id.exists' => 'Le taux de TVA spécifié dans une ligne n\'existe pas.',
];
}
}

View File

@ -0,0 +1,84 @@
<?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'],
'product_id' => ['nullable', 'exists:products,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,93 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateInvoiceRequest 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
{
$invoiceId = $this->route('invoice');
return [
'client_id' => 'nullable|exists:clients,id',
'group_id' => 'nullable|exists:client_groups,id',
'source_quote_id' => 'nullable|exists:quotes,id',
'invoice_number' => 'sometimes|string|max:191|unique:invoices,invoice_number,' . $invoiceId,
'status' => 'sometimes|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir',
'invoice_date' => 'sometimes|date',
'due_date' => 'nullable|date|after_or_equal:invoice_date',
'currency' => 'sometimes|string|size:3',
'total_ht' => 'sometimes|numeric|min:0',
'total_tva' => 'sometimes|numeric|min:0',
'total_ttc' => 'sometimes|numeric|min:0',
'e_invoicing_channel_id' => 'nullable|exists:e_invoicing_channels,id',
'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive',
];
}
public function messages(): array
{
return [
'client_id.exists' => 'Le client sélectionné est invalide.',
'group_id.exists' => 'Le groupe sélectionné est invalide.',
'source_quote_id.exists' => 'Le devis source est invalide.',
'invoice_number.string' => 'Le numéro de facture doit être une chaîne de caractères.',
'invoice_number.max' => 'Le numéro de facture ne doit pas dépasser 191 caractères.',
'invoice_number.unique' => 'Ce numéro de facture existe déjà.',
'status.in' => 'Le statut sélectionné est invalide.',
'invoice_date.date' => 'La date de la facture n\'est pas valide.',
'due_date.date' => 'La date d\'échéance n\'est pas valide.',
'due_date.after_or_equal' => 'La date d\'échéance doit être postérieure ou égale à la date de la facture.',
'currency.size' => 'La devise doit comporter 3 caractères.',
'total_ht.numeric' => 'Le total HT doit être un nombre.',
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
'e_invoicing_channel_id.exists' => 'Le canal de facturation électronique est invalide.',
'e_invoice_status.in' => 'Le statut de facturation électronique est invalide.',
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator) {
if (! $this->hasAny(['client_id', 'group_id'])) {
return;
}
$hasClient = filled($this->input('client_id'));
$hasGroup = filled($this->input('group_id'));
if (! $hasClient && ! $hasGroup) {
$message = 'Un client ou un groupe de clients est obligatoire.';
$validator->errors()->add('client_id', $message);
$validator->errors()->add('group_id', $message);
}
if ($hasClient && $hasGroup) {
$message = 'Selectionnez soit un client, soit un groupe de clients.';
$validator->errors()->add('client_id', $message);
$validator->errors()->add('group_id', $message);
}
});
}
}

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,54 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdatePriceListRequest 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
{
$routePriceList = $this->route('price_list');
$priceListId = is_object($routePriceList)
? $routePriceList->id
: ($routePriceList ?? $this->route('id'));
return [
'name' => [
'required',
'string',
'max:191',
Rule::unique('price_lists', 'name')->ignore($priceListId),
],
'valid_from' => 'nullable|date',
'valid_to' => 'nullable|date|after_or_equal:valid_from',
'is_default' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom de la liste de prix est obligatoire.',
'name.unique' => 'Une liste de prix avec ce nom existe déjà.',
'valid_from.date' => 'La date de début doit être une date valide.',
'valid_to.date' => 'La date de fin doit être une date valide.',
'valid_to.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.',
'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
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
{
return [
'code' => [
'required',
'string',
'max:64',
Rule::unique('product_categories')->ignore($this->route('product_category')),
],
'name' => 'required|string|max:191',
'description' => 'nullable|string',
'parent_id' => [
'nullable',
'exists:product_categories,id',
// Prevent setting parent to itself
function ($attribute, $value, $fail) {
if ($this->route('product_category') && $value == $this->route('product_category')->id) {
$fail('The parent category cannot be the category itself.');
}
},
],
'intervention' => 'boolean',
'active' => 'boolean',
];
}
}

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,48 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePurchaseOrderRequest 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 [
'fournisseur_id' => ['nullable', 'exists:fournisseurs,id'],
'po_number' => ['nullable', 'string', 'max:191', 'unique:purchase_orders,po_number,' . $this->route('purchase_order')],
'status' => ['nullable', 'in:brouillon,confirmee,livree,facturee,annulee'],
'order_date' => ['nullable', 'date'],
'expected_date' => ['nullable', 'date'],
'currency' => ['nullable', 'string', 'size:3'],
'total_ht' => ['nullable', 'numeric', 'min:0'],
'total_tva' => ['nullable', 'numeric', 'min:0'],
'total_ttc' => ['nullable', 'numeric', 'min:0'],
'notes' => ['nullable', 'string'],
'delivery_address' => ['nullable', 'string'],
'lines' => ['nullable', 'array', 'min:1'],
'lines.*.product_id' => ['nullable', 'exists:products,id'],
'lines.*.description' => ['required', 'string'],
'lines.*.quantity' => ['required', 'numeric', 'min:0.001'],
'lines.*.unit_price' => ['required', 'numeric', 'min:0'],
'lines.*.tva_rate' => ['required', 'numeric', 'min:0'],
'lines.*.discount_pct' => ['nullable', 'numeric', 'min:0', 'max:100'],
'lines.*.total_ht' => ['required', 'numeric', 'min:0'],
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateQuoteLineRequest 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 [
'quote_id' => 'sometimes|exists:quotes,id',
'product_id' => 'nullable|exists:products,id',
'packaging_id' => 'nullable|exists:product_packagings,id',
'packages_qty' => 'nullable|numeric|min:0',
'units_qty' => 'nullable|numeric|min:0',
'description' => 'sometimes|string',
'qty_base' => 'nullable|numeric|min:0',
'unit_price' => 'sometimes|numeric|min:0',
'unit_price_per_package' => 'nullable|numeric|min:0',
'tva_rate_id' => 'nullable|exists:tva_rates,id',
'discount_pct' => 'sometimes|numeric|min:0|max:100',
'total_ht' => 'sometimes|numeric|min:0',
];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateQuoteRequest 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
{
$quoteId = $this->route('quote');
return [
'client_id' => 'sometimes|exists:clients,id',
'group_id' => 'nullable|exists:client_groups,id',
'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $quoteId,
'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule',
'quote_date' => 'sometimes|date',
'valid_until' => 'nullable|date|after_or_equal:quote_date',
'currency' => 'sometimes|string|size:3',
'total_ht' => 'sometimes|numeric|min:0',
'total_tva' => 'sometimes|numeric|min:0',
'total_ttc' => 'sometimes|numeric|min:0',
];
}
public function messages(): array
{
return [
'client_id.exists' => 'Le client sélectionné est invalide.',
'group_id.exists' => 'Le groupe sélectionné est invalide.',
'reference.string' => 'Le numéro de devis doit être une chaîne de caractères.',
'reference.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.',
'reference.unique' => 'Ce numéro de devis existe déjà.',
'status.in' => 'Le statut sélectionné est invalide.',
'quote_date.date' => 'La date du devis n\'est pas valide.',
'valid_until.date' => 'La date de validité n\'est pas valide.',
'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.',
'currency.size' => 'La devise doit comporter 3 caractères.',
'total_ht.numeric' => 'Le total HT doit être un nombre.',
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateStockItemRequest 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 [
'qty_on_hand_base' => 'sometimes|numeric|min:0',
'safety_stock_base' => 'sometimes|numeric|min:0',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'qty_on_hand_base.numeric' => 'La quantité en stock doit être un nombre.',
'safety_stock_base.numeric' => 'Le stock de sécurité doit être un nombre.',
];
}
}

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,45 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTvaRateRequest 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|string|max:50',
'rate' => 'sometimes|numeric|min:0|max:999.99',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.string' => 'Le nom doit être une chaîne de caractères.',
'name.max' => 'Le nom ne peut pas dépasser 50 caractères.',
'rate.numeric' => 'Le taux doit être un nombre.',
'rate.min' => 'Le taux ne peut pas être négatif.',
'rate.max' => 'Le taux ne peut pas dépasser 999.99.',
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateWarehouseRequest 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: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',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.required' => 'Le nom de l\'entrepôt est requis.',
'name.string' => 'Le nom doit être une chaîne de caractères.',
'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 comporter exactement 2 caractères.',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class AvoirLineResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'avoir_id' => $this->avoir_id,
'product_id' => $this->product_id,
'invoice_line_id' => $this->invoice_line_id,
'description' => $this->description,
'quantity' => $this->quantity,
'unit_price' => $this->unit_price,
'tva_rate' => $this->tva_rate,
'total_ht' => $this->total_ht,
'total_tva' => $this->total_tva,
'total_ttc' => $this->total_ttc,
'notes' => $this->notes,
'product' => $this->whenLoaded('product'),
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class AvoirResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'client_id' => $this->client_id,
'invoice_id' => $this->invoice_id,
'group_id' => $this->group_id,
'avoir_number' => $this->avoir_number,
'status' => $this->status,
'avoir_date' => $this->avoir_date,
'due_date' => $this->due_date,
'currency' => $this->currency,
'total_ht' => $this->total_ht,
'total_tva' => $this->total_tva,
'total_ttc' => $this->total_ttc,
'reason_type' => $this->reason_type,
'reason_description' => $this->reason_description,
'e_invoice_status' => $this->e_invoice_status,
'refund_status' => $this->refund_status,
'refund_date' => $this->refund_date,
'refund_method' => $this->refund_method,
'compensation_invoice_id' => $this->compensation_invoice_id,
'compensation_amount' => $this->compensation_amount,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'client' => $this->whenLoaded('client'),
'invoice' => $this->whenLoaded('invoice'),
'group' => $this->whenLoaded('group'),
'lines' => AvoirLineResource::collection($this->whenLoaded('lines')),
'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')),
];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Resources\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClientActivityTimelineResource 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,
'actor_type' => $this->actor_type,
'actor_name' => $this->actor ? $this->actor->firstname . ' ' . $this->actor->lastname : 'System',
'event_type' => $this->event_type,
'entity_type' => $this->entity_type,
'entity_id' => $this->entity_id,
'title' => $this->title,
'description' => $this->description,
'metadata' => $this->metadata,
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'time_ago' => $this->created_at?->diffForHumans(),
// Helper properties for frontend display
'icon' => $this->getIcon(),
'color' => $this->getColor(),
];
}
protected function getIcon()
{
// Map event types to icons (using Nucleo icons as requested)
return match($this->event_type) {
'call' => 'mobile-button',
'email_sent', 'email_received' => 'email-83',
'invoice_created', 'invoice_sent', 'invoice_paid' => 'money-coins',
'quote_created', 'quote_sent' => 'single-copy-04',
'file_uploaded', 'attachment_sent', 'attachment_received' => 'cloud-upload-96',
'client_created' => 'badge-24',
'meeting', 'intervention_created' => 'laptop',
default => 'bell-55'
};
}
protected function getColor()
{
// Map event types to colors
return match($this->event_type) {
'client_created' => 'success',
'call' => 'info',
'email_sent' => 'success',
'invoice_paid' => 'success',
'invoice_created' => 'warning',
'quote_created' => 'primary',
'quote_sent' => 'primary',
default => 'dark'
};
}
}

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,29 @@
<?php
namespace App\Http\Resources\Client;
use App\Http\Resources\Client\ClientResource;
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,
'clients_count' => $this->whenCounted('clients'),
'client_ids' => $this->whenLoaded('clients', fn () => $this->clients->pluck('id')->values()),
'clients' => ClientResource::collection($this->whenLoaded('clients')),
'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,67 @@
<?php
namespace App\Http\Resources\Client;
use App\Http\Resources\Contact\ContactResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
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,
'avatar_url' => $this->avatar ? Storage::url($this->avatar) : null,
'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,
'is_parent' => $this->is_parent,
'parent_id' => $this->parent_id,
// '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')),
'parent' => new ClientResource($this->whenLoaded('parent')),
// '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,25 @@
<?php
namespace App\Http\Resources;
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,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

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

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