feat: initialisation complète du projet CESIZen
- Backend Laravel 11 (API REST, JWT auth, PostgreSQL) - Frontend Next.js 14 (React, TypeScript, Tailwind CSS) - Docker Compose (PostgreSQL, backend, frontend) - Modules : auth, profil, tracker émotions, informations, admin
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Emotion;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdminEmotionController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$emotions = Emotion::niveau(1)
|
||||
->with(['enfants' => fn($q) => $q->orderBy('nom')])
|
||||
->orderBy('nom')
|
||||
->get();
|
||||
|
||||
return response()->json($emotions);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'nom' => 'required|string|max:255',
|
||||
'couleur' => 'required|string|max:7',
|
||||
'icone' => 'nullable|string|max:10',
|
||||
'niveau' => 'required|integer|in:1,2',
|
||||
'parent_id' => 'nullable|required_if:niveau,2|exists:emotions,id',
|
||||
]);
|
||||
|
||||
$emotion = Emotion::create($request->only('nom', 'couleur', 'icone', 'niveau', 'parent_id'));
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Émotion créée',
|
||||
'emotion' => $emotion,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$emotion = Emotion::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'nom' => 'sometimes|string|max:255',
|
||||
'couleur' => 'sometimes|string|max:7',
|
||||
'icone' => 'nullable|string|max:10',
|
||||
'est_actif' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
$emotion->update($request->only('nom', 'couleur', 'icone', 'est_actif'));
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Émotion modifiée',
|
||||
'emotion' => $emotion,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$emotion = Emotion::findOrFail($id);
|
||||
$emotion->update(['est_actif' => false]);
|
||||
$emotion->enfants()->update(['est_actif' => false]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Émotion désactivée',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreFeedRequest;
|
||||
use App\Models\Feed;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminFeedController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$feeds = Feed::with('auteur:id,nom,prenom')
|
||||
->orderBy('ordre')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json($feeds);
|
||||
}
|
||||
|
||||
public function store(StoreFeedRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$data['auteur_id'] = auth()->id();
|
||||
$data['slug'] = $data['slug'] ?? Str::slug($data['titre']);
|
||||
|
||||
$feed = Feed::create($data);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Contenu créé',
|
||||
'feed' => $feed,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(StoreFeedRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$feed = Feed::findOrFail($id);
|
||||
$data = $request->validated();
|
||||
|
||||
if (isset($data['titre']) && !isset($data['slug'])) {
|
||||
$data['slug'] = Str::slug($data['titre']);
|
||||
}
|
||||
|
||||
$feed->update($data);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Contenu modifié',
|
||||
'feed' => $feed,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
Feed::findOrFail($id)->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Contenu supprimé',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Utilisateur;
|
||||
use App\Models\Role;
|
||||
use App\Models\Tracker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AdminUtilisateurController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Utilisateur::with('role');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nom', 'ilike', "%{$search}%")
|
||||
->orWhere('prenom', 'ilike', "%{$search}%")
|
||||
->orWhere('email', 'ilike', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('role')) {
|
||||
$query->whereHas('role', fn($q) => $q->where('nom', $request->role));
|
||||
}
|
||||
|
||||
$utilisateurs = $query->orderBy('created_at', 'desc')->paginate(15);
|
||||
|
||||
return response()->json($utilisateurs);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$utilisateur = Utilisateur::with('role')->findOrFail($id);
|
||||
|
||||
return response()->json($utilisateur);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'nom' => 'required|string|max:255',
|
||||
'prenom' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:utilisateurs,email',
|
||||
'password' => 'required|string|min:8',
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
]);
|
||||
|
||||
$utilisateur = Utilisateur::create($request->only('nom', 'prenom', 'email', 'password', 'role_id'));
|
||||
|
||||
Tracker::create([
|
||||
'utilisateur_id' => $utilisateur->id,
|
||||
'nom' => 'Mon journal',
|
||||
]);
|
||||
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Utilisateur créé',
|
||||
'utilisateur' => $utilisateur,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$utilisateur = Utilisateur::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'nom' => 'sometimes|string|max:255',
|
||||
'prenom' => 'sometimes|string|max:255',
|
||||
'email' => "sometimes|email|unique:utilisateurs,email,{$id}",
|
||||
'role_id' => 'sometimes|exists:roles,id',
|
||||
]);
|
||||
|
||||
$utilisateur->update($request->only('nom', 'prenom', 'email', 'role_id'));
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Utilisateur modifié',
|
||||
'utilisateur' => $utilisateur,
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): JsonResponse
|
||||
{
|
||||
$utilisateur = Utilisateur::findOrFail($id);
|
||||
$utilisateur->update(['est_actif' => !$utilisateur->est_actif]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $utilisateur->est_actif ? 'Compte activé' : 'Compte désactivé',
|
||||
'utilisateur' => $utilisateur,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$utilisateur = Utilisateur::findOrFail($id);
|
||||
$utilisateur->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Utilisateur supprimé',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\RegisterRequest;
|
||||
use App\Http\Requests\LoginRequest;
|
||||
use App\Models\Utilisateur;
|
||||
use App\Models\Role;
|
||||
use App\Models\Tracker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function register(RegisterRequest $request): JsonResponse
|
||||
{
|
||||
$role = Role::where('nom', 'membre')->first();
|
||||
|
||||
$utilisateur = Utilisateur::create([
|
||||
'nom' => $request->nom,
|
||||
'prenom' => $request->prenom,
|
||||
'email' => $request->email,
|
||||
'password' => $request->password,
|
||||
'role_id' => $role->id,
|
||||
'consentement_rgpd' => true,
|
||||
]);
|
||||
|
||||
Tracker::create([
|
||||
'utilisateur_id' => $utilisateur->id,
|
||||
'nom' => 'Mon journal',
|
||||
]);
|
||||
|
||||
$token = JWTAuth::fromUser($utilisateur);
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Inscription réussie',
|
||||
'utilisateur' => $utilisateur,
|
||||
'token' => $token,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->only('email', 'password');
|
||||
|
||||
if (!$token = auth()->attempt($credentials)) {
|
||||
return response()->json([
|
||||
'message' => 'Identifiants incorrects',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$utilisateur = auth()->user();
|
||||
|
||||
if (!$utilisateur->est_actif) {
|
||||
auth()->logout();
|
||||
return response()->json([
|
||||
'message' => 'Votre compte a été désactivé',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json([
|
||||
'utilisateur' => $utilisateur,
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(): JsonResponse
|
||||
{
|
||||
auth()->logout();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Déconnexion réussie',
|
||||
]);
|
||||
}
|
||||
|
||||
public function me(): JsonResponse
|
||||
{
|
||||
$utilisateur = auth()->user();
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json($utilisateur);
|
||||
}
|
||||
|
||||
public function refresh(): JsonResponse
|
||||
{
|
||||
$token = auth()->refresh();
|
||||
$utilisateur = auth()->user();
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json([
|
||||
'utilisateur' => $utilisateur,
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Emotion;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class EmotionController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$emotions = Emotion::actives()
|
||||
->niveau(1)
|
||||
->with(['enfants' => function ($query) {
|
||||
$query->actives()->orderBy('nom');
|
||||
}])
|
||||
->orderBy('nom')
|
||||
->get();
|
||||
|
||||
return response()->json($emotions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Feed;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class FeedController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$feeds = Feed::publie()
|
||||
->orderBy('ordre')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get(['id', 'titre', 'slug', 'contenu', 'image_url', 'ordre', 'created_at']);
|
||||
|
||||
return response()->json($feeds);
|
||||
}
|
||||
|
||||
public function show(string $slug): JsonResponse
|
||||
{
|
||||
$feed = Feed::publie()->where('slug', $slug)->with('auteur:id,nom,prenom')->firstOrFail();
|
||||
|
||||
return response()->json($feed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateProfilRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class ProfilController extends Controller
|
||||
{
|
||||
public function show(): JsonResponse
|
||||
{
|
||||
$utilisateur = auth()->user();
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json($utilisateur);
|
||||
}
|
||||
|
||||
public function update(UpdateProfilRequest $request): JsonResponse
|
||||
{
|
||||
$utilisateur = auth()->user();
|
||||
$utilisateur->update($request->validated());
|
||||
$utilisateur->load('role');
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Profil mis à jour',
|
||||
'utilisateur' => $utilisateur,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'ancien_mot_de_passe' => 'required|string',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
$utilisateur = auth()->user();
|
||||
|
||||
if (!Hash::check($request->ancien_mot_de_passe, $utilisateur->password)) {
|
||||
return response()->json([
|
||||
'message' => 'L\'ancien mot de passe est incorrect',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$utilisateur->update(['password' => $request->password]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Mot de passe modifié avec succès',
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(): JsonResponse
|
||||
{
|
||||
$utilisateur = auth()->user();
|
||||
$utilisateur->delete();
|
||||
auth()->logout();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Compte supprimé avec succès',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreSaisieRequest;
|
||||
use App\Models\SaisieTracker;
|
||||
use App\Services\RapportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SaisieTrackerController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tracker = auth()->user()->tracker;
|
||||
|
||||
if (!$tracker) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$query = $tracker->saisies()->with('emotion')->orderBy('date_saisie', 'desc');
|
||||
|
||||
if ($request->filled('date_debut')) {
|
||||
$query->where('date_saisie', '>=', $request->date_debut);
|
||||
}
|
||||
if ($request->filled('date_fin')) {
|
||||
$query->where('date_saisie', '<=', $request->date_fin);
|
||||
}
|
||||
if ($request->filled('emotion_id')) {
|
||||
$query->where('emotion_id', $request->emotion_id);
|
||||
}
|
||||
|
||||
$saisies = $query->paginate(20);
|
||||
|
||||
return response()->json($saisies);
|
||||
}
|
||||
|
||||
public function store(StoreSaisieRequest $request): JsonResponse
|
||||
{
|
||||
$tracker = auth()->user()->tracker;
|
||||
|
||||
if (!$tracker) {
|
||||
$tracker = auth()->user()->tracker()->create(['nom' => 'Mon journal']);
|
||||
}
|
||||
|
||||
$saisie = $tracker->saisies()->create($request->validated());
|
||||
$saisie->load('emotion');
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Saisie enregistrée',
|
||||
'saisie' => $saisie,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(StoreSaisieRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$saisie = auth()->user()->tracker->saisies()->findOrFail($id);
|
||||
$saisie->update($request->validated());
|
||||
$saisie->load('emotion');
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Saisie modifiée',
|
||||
'saisie' => $saisie,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$saisie = auth()->user()->tracker->saisies()->findOrFail($id);
|
||||
$saisie->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Saisie supprimée',
|
||||
]);
|
||||
}
|
||||
|
||||
public function rapports(Request $request, RapportService $rapportService): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'period' => 'sometimes|in:week,month,quarter,year',
|
||||
]);
|
||||
|
||||
$period = $request->get('period', 'month');
|
||||
$rapport = $rapportService->genererRapport(auth()->user(), $period);
|
||||
|
||||
return response()->json($rapport);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ForceJsonResponse
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
|
||||
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
|
||||
use Tymon\JWTAuth\Exceptions\JWTException;
|
||||
|
||||
class JwtMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
try {
|
||||
JWTAuth::parseToken()->authenticate();
|
||||
} catch (TokenExpiredException $e) {
|
||||
return response()->json(['message' => 'Token expiré'], 401);
|
||||
} catch (TokenInvalidException $e) {
|
||||
return response()->json(['message' => 'Token invalide'], 401);
|
||||
} catch (JWTException $e) {
|
||||
return response()->json(['message' => 'Token absent'], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string ...$roles)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user || !in_array($user->role->nom, $roles)) {
|
||||
return response()->json(['message' => 'Accès non autorisé'], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => 'L\'adresse email est obligatoire.',
|
||||
'email.email' => 'L\'adresse email n\'est pas valide.',
|
||||
'password.required' => 'Le mot de passe est obligatoire.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RegisterRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nom' => 'required|string|max:255',
|
||||
'prenom' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:utilisateurs,email',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'consentement_rgpd' => 'required|accepted',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'nom.required' => 'Le nom est obligatoire.',
|
||||
'prenom.required' => 'Le prénom est obligatoire.',
|
||||
'email.required' => 'L\'adresse email est obligatoire.',
|
||||
'email.email' => 'L\'adresse email n\'est pas valide.',
|
||||
'email.unique' => 'Cette adresse email est déjà utilisée.',
|
||||
'password.required' => 'Le mot de passe est obligatoire.',
|
||||
'password.min' => 'Le mot de passe doit contenir au moins 8 caractères.',
|
||||
'password.confirmed' => 'La confirmation du mot de passe ne correspond pas.',
|
||||
'consentement_rgpd.required' => 'Vous devez accepter le traitement de vos données.',
|
||||
'consentement_rgpd.accepted' => 'Vous devez accepter le traitement de vos données.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreFeedRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'titre' => 'required|string|max:255',
|
||||
'contenu' => 'required|string',
|
||||
'image_url' => 'nullable|string|max:500',
|
||||
'est_publie' => 'boolean',
|
||||
'ordre' => 'integer|min:0',
|
||||
];
|
||||
|
||||
if ($this->isMethod('POST')) {
|
||||
$rules['slug'] = 'nullable|string|max:255|unique:feeds,slug';
|
||||
} else {
|
||||
$rules['slug'] = [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('feeds', 'slug')->ignore($this->route('id')),
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreSaisieRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'emotion_id' => 'required|exists:emotions,id',
|
||||
'intensite' => 'required|integer|between:1,10',
|
||||
'note' => 'nullable|string|max:2000',
|
||||
'date_saisie' => 'required|date',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'emotion_id.required' => 'L\'émotion est obligatoire.',
|
||||
'emotion_id.exists' => 'Cette émotion n\'existe pas.',
|
||||
'intensite.required' => 'L\'intensité est obligatoire.',
|
||||
'intensite.between' => 'L\'intensité doit être entre 1 et 10.',
|
||||
'date_saisie.required' => 'La date est obligatoire.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProfilRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nom' => 'sometimes|string|max:255',
|
||||
'prenom' => 'sometimes|string|max:255',
|
||||
'email' => [
|
||||
'sometimes',
|
||||
'email',
|
||||
Rule::unique('utilisateurs', 'email')->ignore(auth()->id()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Audit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'audits';
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'utilisateur_id',
|
||||
'action',
|
||||
'table_cible',
|
||||
'enregistrement_id',
|
||||
'anciennes_valeurs',
|
||||
'nouvelles_valeurs',
|
||||
'ip_address',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'anciennes_valeurs' => 'array',
|
||||
'nouvelles_valeurs' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function utilisateur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Utilisateur::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ContactUrgence extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'contacts_urgence';
|
||||
|
||||
protected $fillable = [
|
||||
'utilisateur_id',
|
||||
'nom',
|
||||
'telephone',
|
||||
'relation',
|
||||
];
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function utilisateur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Utilisateur::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Emotion extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'emotions';
|
||||
|
||||
protected $fillable = [
|
||||
'nom',
|
||||
'couleur',
|
||||
'icone',
|
||||
'niveau',
|
||||
'parent_id',
|
||||
'est_actif',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'niveau' => 'integer',
|
||||
'est_actif' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Emotion::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function enfants(): HasMany
|
||||
{
|
||||
return $this->hasMany(Emotion::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function saisieTrackers(): HasMany
|
||||
{
|
||||
return $this->hasMany(SaisieTracker::class);
|
||||
}
|
||||
|
||||
// ──── Scopes ────
|
||||
|
||||
public function scopeActives(Builder $query): Builder
|
||||
{
|
||||
return $query->where('est_actif', true);
|
||||
}
|
||||
|
||||
public function scopeNiveau(Builder $query, int $niveau): Builder
|
||||
{
|
||||
return $query->where('niveau', $niveau);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Feed extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'feeds';
|
||||
|
||||
protected $fillable = [
|
||||
'titre',
|
||||
'slug',
|
||||
'contenu',
|
||||
'image_url',
|
||||
'est_publie',
|
||||
'auteur_id',
|
||||
'ordre',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'est_publie' => 'boolean',
|
||||
'ordre' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function auteur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Utilisateur::class, 'auteur_id');
|
||||
}
|
||||
|
||||
// ──── Scopes ────
|
||||
|
||||
public function scopePublie(Builder $query): Builder
|
||||
{
|
||||
return $query->where('est_publie', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'roles';
|
||||
|
||||
protected $fillable = [
|
||||
'nom',
|
||||
'description',
|
||||
];
|
||||
|
||||
public function utilisateurs(): HasMany
|
||||
{
|
||||
return $this->hasMany(Utilisateur::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SaisieTracker extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'saisie_trackers';
|
||||
|
||||
protected $fillable = [
|
||||
'tracker_id',
|
||||
'emotion_id',
|
||||
'intensite',
|
||||
'note',
|
||||
'date_saisie',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'date_saisie' => 'datetime',
|
||||
'intensite' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function tracker(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tracker::class);
|
||||
}
|
||||
|
||||
public function emotion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Emotion::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Tracker extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'trackers';
|
||||
|
||||
protected $fillable = [
|
||||
'utilisateur_id',
|
||||
'nom',
|
||||
];
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function utilisateur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Utilisateur::class);
|
||||
}
|
||||
|
||||
public function saisies(): HasMany
|
||||
{
|
||||
return $this->hasMany(SaisieTracker::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||
|
||||
class Utilisateur extends Authenticatable implements JWTSubject
|
||||
{
|
||||
use HasFactory, Notifiable, SoftDeletes;
|
||||
|
||||
protected $table = 'utilisateurs';
|
||||
|
||||
protected $fillable = [
|
||||
'nom',
|
||||
'prenom',
|
||||
'email',
|
||||
'password',
|
||||
'role_id',
|
||||
'est_actif',
|
||||
'consentement_rgpd',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'est_actif' => 'boolean',
|
||||
'consentement_rgpd' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
// ──── JWTSubject ────
|
||||
|
||||
public function getJWTIdentifier(): mixed
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
public function getJWTCustomClaims(): array
|
||||
{
|
||||
return [
|
||||
'role' => $this->role->nom,
|
||||
];
|
||||
}
|
||||
|
||||
// ──── Relations ────
|
||||
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
public function tracker(): HasOne
|
||||
{
|
||||
return $this->hasOne(Tracker::class);
|
||||
}
|
||||
|
||||
public function contactsUrgence(): HasMany
|
||||
{
|
||||
return $this->hasMany(ContactUrgence::class);
|
||||
}
|
||||
|
||||
// ──── Helpers ────
|
||||
|
||||
public function estAdmin(): bool
|
||||
{
|
||||
return $this->role->nom === 'administrateur';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Utilisateur;
|
||||
use App\Models\SaisieTracker;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RapportService
|
||||
{
|
||||
public function genererRapport(Utilisateur $user, string $period): array
|
||||
{
|
||||
$tracker = $user->tracker;
|
||||
|
||||
if (!$tracker) {
|
||||
return $this->rapportVide();
|
||||
}
|
||||
|
||||
$dateDebut = $this->getDateDebut($period);
|
||||
$dateFin = Carbon::now();
|
||||
|
||||
$saisies = $tracker->saisies()
|
||||
->with('emotion')
|
||||
->where('date_saisie', '>=', $dateDebut)
|
||||
->where('date_saisie', '<=', $dateFin)
|
||||
->orderBy('date_saisie')
|
||||
->get();
|
||||
|
||||
if ($saisies->isEmpty()) {
|
||||
return $this->rapportVide();
|
||||
}
|
||||
|
||||
$totalSaisies = $saisies->count();
|
||||
$intensiteMoyenne = round($saisies->avg('intensite'), 1);
|
||||
|
||||
$emotionsDominantes = $saisies->groupBy('emotion_id')
|
||||
->map(fn($group) => [
|
||||
'emotion' => $group->first()->emotion,
|
||||
'count' => $group->count(),
|
||||
'intensite_moyenne' => round($group->avg('intensite'), 1),
|
||||
])
|
||||
->sortByDesc('count')
|
||||
->values();
|
||||
|
||||
$emotionDominante = $emotionsDominantes->first();
|
||||
|
||||
$evolution = $this->getEvolution($saisies, $period, $dateDebut, $dateFin);
|
||||
|
||||
$repartition = $emotionsDominantes->map(fn($item) => [
|
||||
'nom' => $item['emotion']->nom,
|
||||
'couleur' => $item['emotion']->couleur,
|
||||
'icone' => $item['emotion']->icone,
|
||||
'count' => $item['count'],
|
||||
'pourcentage' => round(($item['count'] / $totalSaisies) * 100, 1),
|
||||
'intensite_moyenne' => $item['intensite_moyenne'],
|
||||
])->values();
|
||||
|
||||
return [
|
||||
'periode' => $period,
|
||||
'date_debut' => $dateDebut->toDateString(),
|
||||
'date_fin' => $dateFin->toDateString(),
|
||||
'stats' => [
|
||||
'total_saisies' => $totalSaisies,
|
||||
'intensite_moyenne' => $intensiteMoyenne,
|
||||
'emotion_dominante' => $emotionDominante ? [
|
||||
'nom' => $emotionDominante['emotion']->nom,
|
||||
'couleur' => $emotionDominante['emotion']->couleur,
|
||||
'icone' => $emotionDominante['emotion']->icone,
|
||||
'count' => $emotionDominante['count'],
|
||||
] : null,
|
||||
],
|
||||
'repartition' => $repartition,
|
||||
'evolution' => $evolution,
|
||||
];
|
||||
}
|
||||
|
||||
private function getDateDebut(string $period): Carbon
|
||||
{
|
||||
return match ($period) {
|
||||
'week' => Carbon::now()->subWeek(),
|
||||
'month' => Carbon::now()->subMonth(),
|
||||
'quarter' => Carbon::now()->subMonths(3),
|
||||
'year' => Carbon::now()->subYear(),
|
||||
default => Carbon::now()->subMonth(),
|
||||
};
|
||||
}
|
||||
|
||||
private function getEvolution($saisies, string $period, Carbon $dateDebut, Carbon $dateFin): array
|
||||
{
|
||||
$format = match ($period) {
|
||||
'week' => 'Y-m-d',
|
||||
'month' => 'Y-m-d',
|
||||
'quarter' => 'Y-W',
|
||||
'year' => 'Y-m',
|
||||
default => 'Y-m-d',
|
||||
};
|
||||
|
||||
$grouped = $saisies->groupBy(fn($saisie) => $saisie->date_saisie->format($format));
|
||||
|
||||
$labels = [];
|
||||
$dataIntensiteMoyenne = [];
|
||||
|
||||
foreach ($grouped as $label => $group) {
|
||||
$labels[] = $label;
|
||||
$dataIntensiteMoyenne[] = round($group->avg('intensite'), 1);
|
||||
}
|
||||
|
||||
$emotionDatasets = $saisies->groupBy('emotion_id')->map(function ($emotionGroup) use ($format, $grouped) {
|
||||
$emotion = $emotionGroup->first()->emotion;
|
||||
$byPeriod = $emotionGroup->groupBy(fn($s) => $s->date_saisie->format($format));
|
||||
|
||||
$data = [];
|
||||
foreach ($grouped->keys() as $label) {
|
||||
$data[] = isset($byPeriod[$label]) ? $byPeriod[$label]->count() : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'nom' => $emotion->nom,
|
||||
'couleur' => $emotion->couleur,
|
||||
'data' => $data,
|
||||
];
|
||||
})->values();
|
||||
|
||||
return [
|
||||
'labels' => $labels,
|
||||
'intensite_moyenne' => $dataIntensiteMoyenne,
|
||||
'emotions' => $emotionDatasets,
|
||||
];
|
||||
}
|
||||
|
||||
private function rapportVide(): array
|
||||
{
|
||||
return [
|
||||
'periode' => 'month',
|
||||
'date_debut' => Carbon::now()->subMonth()->toDateString(),
|
||||
'date_fin' => Carbon::now()->toDateString(),
|
||||
'stats' => [
|
||||
'total_saisies' => 0,
|
||||
'intensite_moyenne' => 0,
|
||||
'emotion_dominante' => null,
|
||||
],
|
||||
'repartition' => [],
|
||||
'evolution' => [
|
||||
'labels' => [],
|
||||
'intensite_moyenne' => [],
|
||||
'emotions' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user