Initial commit with contrats and domaines modules

This commit is contained in:
mrKamoo
2026-04-08 18:07:08 +02:00
commit 092a6a0484
191 changed files with 24639 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\Categorie;
use App\Models\Fournisseur;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ArticleController extends Controller
{
public function index(Request $request): Response
{
$articles = Article::with(['categorie', 'fournisseur'])
->when($request->search, fn ($q) => $q->where(function ($sub) use ($request) {
$sub->where('designation', 'like', "%{$request->search}%")
->orWhere('reference', 'like', "%{$request->search}%");
}))
->when($request->categorie_id, fn ($q) => $q->where('categorie_id', $request->categorie_id))
->orderBy('designation')
->paginate(20)
->withQueryString();
return Inertia::render('Articles/Index', [
'articles' => $articles,
'categories' => Categorie::active()->orderBy('ordre')->get(),
'filters' => $request->only(['search', 'categorie_id']),
]);
}
public function create(): Response
{
return Inertia::render('Articles/Create', [
'categories' => Categorie::active()->orderBy('ordre')->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'reference' => 'nullable|string|max:100',
'designation' => 'required|string|max:255',
'description' => 'nullable|string',
'categorie_id' => 'nullable|exists:categories,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'prix_unitaire_ht' => 'nullable|numeric|min:0',
'unite' => 'nullable|string|max:30',
]);
Article::create($validated);
return redirect()->route('articles.index')
->with('success', 'Article créé avec succès.');
}
public function edit(Article $article): Response
{
return Inertia::render('Articles/Edit', [
'article' => $article,
'categories' => Categorie::active()->orderBy('ordre')->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
]);
}
public function update(Request $request, Article $article): RedirectResponse
{
$validated = $request->validate([
'reference' => 'nullable|string|max:100',
'designation' => 'required|string|max:255',
'description' => 'nullable|string',
'categorie_id' => 'nullable|exists:categories,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'prix_unitaire_ht' => 'nullable|numeric|min:0',
'unite' => 'nullable|string|max:30',
'active' => 'boolean',
]);
$article->update($validated);
return redirect()->route('articles.index')
->with('success', 'Article mis à jour.');
}
public function destroy(Article $article): RedirectResponse
{
$article->update(['active' => false]);
return back()->with('success', 'Article désactivé.');
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): Response
{
return Inertia::render('Auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Models\Categorie;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class CategorieController extends Controller
{
public function index(): Response
{
return Inertia::render('Categories/Index', [
'categories' => Categorie::withCount('articles')->orderBy('ordre')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Categorie::class);
$validated = $request->validate([
'nom' => 'required|string|max:100|unique:categories,nom',
'description' => 'nullable|string',
'couleur' => 'nullable|string|max:7',
'icone' => 'nullable|string|max:50',
'ordre' => 'nullable|integer|min:0',
]);
Categorie::create($validated);
return back()->with('success', 'Catégorie créée.');
}
public function update(Request $request, Categorie $categorie): RedirectResponse
{
$this->authorize('update', Categorie::class);
$validated = $request->validate([
'nom' => 'required|string|max:100|unique:categories,nom,' . $categorie->id,
'description' => 'nullable|string',
'couleur' => 'nullable|string|max:7',
'icone' => 'nullable|string|max:50',
'ordre' => 'nullable|integer|min:0',
'active' => 'boolean',
]);
$categorie->update($validated);
return back()->with('success', 'Catégorie mise à jour.');
}
public function destroy(Categorie $categorie): RedirectResponse
{
$this->authorize('delete', Categorie::class);
$categorie->update(['active' => false]);
return back()->with('success', 'Catégorie désactivée.');
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\Categorie;
use App\Models\Commande;
use App\Models\Fournisseur;
use App\Models\Service;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class CommandeController extends Controller
{
public function index(Request $request): Response
{
$query = Commande::with(['service', 'fournisseur', 'demandeur'])
->when($request->service_id, fn ($q) => $q->parService($request->service_id))
->when($request->fournisseur_id, fn ($q) => $q->parFournisseur($request->fournisseur_id))
->when($request->statut, fn ($q) => $q->parStatut($request->statut))
->when($request->priorite, fn ($q) => $q->where('priorite', $request->priorite))
->when($request->date_from, fn ($q) => $q->whereDate('date_demande', '>=', $request->date_from))
->when($request->date_to, fn ($q) => $q->whereDate('date_demande', '<=', $request->date_to))
->when($request->search, function ($q) use ($request) {
$q->where(function ($sub) use ($request) {
$sub->where('numero_commande', 'like', "%{$request->search}%")
->orWhere('objet', 'like', "%{$request->search}%")
->orWhereHas('fournisseur', fn ($f) => $f->where('nom', 'like', "%{$request->search}%"));
});
});
$commandes = $query->latest()->paginate(20)->withQueryString();
return Inertia::render('Commandes/Index', [
'commandes' => $commandes,
'services' => Service::all(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut', 'priorite', 'date_from', 'date_to']),
]);
}
public function create(): Response
{
$this->authorize('create', Commande::class);
return Inertia::render('Commandes/Create', [
'services' => Service::all(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'categories' => Categorie::active()->orderBy('ordre')->get(),
'articles' => Article::active()->with('categorie')->orderBy('designation')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Commande::class);
$validated = $request->validate([
'service_id' => 'required|exists:services,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'objet' => 'required|string|max:255',
'description' => 'nullable|string',
'justification' => 'nullable|string',
'priorite' => 'required|in:normale,haute,urgente',
'reference_fournisseur' => 'nullable|string|max:100',
'imputation_budgetaire' => 'nullable|string|max:100',
'date_demande' => 'required|date',
'date_souhaitee' => 'nullable|date|after_or_equal:date_demande',
'date_livraison_prevue' => 'nullable|date',
'notes' => 'nullable|string',
'notes_fournisseur' => 'nullable|string',
'lignes' => 'array',
'lignes.*.designation' => 'required|string|max:255',
'lignes.*.reference' => 'nullable|string|max:100',
'lignes.*.quantite' => 'required|numeric|min:0.001',
'lignes.*.unite' => 'nullable|string|max:30',
'lignes.*.prix_unitaire_ht' => 'nullable|numeric|min:0',
'lignes.*.taux_tva' => 'nullable|numeric|min:0|max:100',
'lignes.*.categorie_id' => 'nullable|exists:categories,id',
'lignes.*.article_id' => 'nullable|exists:articles,id',
'lignes.*.notes' => 'nullable|string',
]);
DB::transaction(function () use ($validated, $request) {
$commande = Commande::create([
...$validated,
'numero_commande' => Commande::genererNumero(),
'user_id' => $request->user()->id,
'statut' => 'brouillon',
]);
foreach ($validated['lignes'] ?? [] as $index => $ligne) {
$commande->lignes()->create(array_merge($ligne, ['ordre' => $index]));
}
});
return redirect()->route('commandes.index')
->with('success', 'Commande créée avec succès.');
}
public function show(Commande $commande): Response
{
$this->authorize('view', $commande);
$commande->load([
'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur',
'lignes.categorie',
'historique.user',
'piecesJointes.user',
]);
$transitionsDisponibles = collect(Commande::STATUT_TRANSITIONS[$commande->statut] ?? [])
->filter(fn ($statut) => request()->user()->hasRole('admin') || $this->userCanTransition(request()->user(), $commande, $statut))
->values();
return Inertia::render('Commandes/Show', [
'commande' => $commande,
'transitionsDisponibles' => $transitionsDisponibles,
]);
}
public function edit(Commande $commande): Response
{
$this->authorize('update', $commande);
$commande->load('lignes.categorie');
return Inertia::render('Commandes/Edit', [
'commande' => $commande,
'services' => Service::all(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'categories' => Categorie::active()->orderBy('ordre')->get(),
'articles' => Article::active()->with('categorie')->orderBy('designation')->get(),
]);
}
public function update(Request $request, Commande $commande): RedirectResponse
{
$this->authorize('update', $commande);
$validated = $request->validate([
'service_id' => 'required|exists:services,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'objet' => 'required|string|max:255',
'description' => 'nullable|string',
'justification' => 'nullable|string',
'priorite' => 'required|in:normale,haute,urgente',
'reference_fournisseur' => 'nullable|string|max:100',
'imputation_budgetaire' => 'nullable|string|max:100',
'date_demande' => 'required|date',
'date_souhaitee' => 'nullable|date',
'date_livraison_prevue' => 'nullable|date',
'notes' => 'nullable|string',
'notes_fournisseur' => 'nullable|string',
'lignes' => 'array',
'lignes.*.id' => 'nullable|integer',
'lignes.*.designation' => 'required|string|max:255',
'lignes.*.reference' => 'nullable|string|max:100',
'lignes.*.quantite' => 'required|numeric|min:0.001',
'lignes.*.quantite_recue' => 'nullable|numeric|min:0',
'lignes.*.unite' => 'nullable|string|max:30',
'lignes.*.prix_unitaire_ht' => 'nullable|numeric|min:0',
'lignes.*.taux_tva' => 'nullable|numeric|min:0|max:100',
'lignes.*.categorie_id' => 'nullable|exists:categories,id',
'lignes.*.article_id' => 'nullable|exists:articles,id',
'lignes.*.notes' => 'nullable|string',
]);
DB::transaction(function () use ($validated, $commande) {
$commande->update($validated);
$lignesData = $validated['lignes'] ?? [];
$lignesIds = collect($lignesData)->pluck('id')->filter()->all();
// Supprimer les lignes retirées
$commande->lignes()->whereNotIn('id', $lignesIds)->delete();
foreach ($lignesData as $index => $ligneData) {
$commande->lignes()->updateOrCreate(
['id' => $ligneData['id'] ?? null],
array_merge($ligneData, ['ordre' => $index, 'commande_id' => $commande->id])
);
}
});
return redirect()->route('commandes.show', $commande)
->with('success', 'Commande mise à jour.');
}
public function destroy(Commande $commande): RedirectResponse
{
$this->authorize('delete', $commande);
$commande->delete();
return redirect()->route('commandes.index')
->with('success', 'Commande supprimée.');
}
public function transition(Request $request, Commande $commande): RedirectResponse
{
$validated = $request->validate([
'statut' => 'required|string',
'commentaire' => 'nullable|string|max:1000',
]);
$this->authorize('transition', [$commande, $validated['statut']]);
$ok = $commande->transitionnerVers($validated['statut'], $request->user(), $validated['commentaire'] ?? null);
if (!$ok) {
return back()->with('error', 'Transition non autorisée.');
}
return redirect()->route('commandes.show', $commande)
->with('success', 'Statut mis à jour : ' . Commande::STATUTS_LABELS[$validated['statut']]);
}
private function userCanTransition($user, Commande $commande, string $statut): bool
{
try {
$this->authorize('transition', [$commande, $statut]);
return true;
} catch (\Exception) {
return false;
}
}
public function exportPdf(Commande $commande)
{
$this->authorize('view', $commande);
$commande->load([
'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur',
'lignes.categorie',
]);
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.commande', [
'commande' => $commande,
]);
return $pdf->stream('commande-' . $commande->numero_commande . '.pdf');
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contrat;
use App\Models\Fournisseur;
use App\Models\Service;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Support\Facades\DB;
class ContratController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('viewAny', Contrat::class);
$query = Contrat::with(['fournisseur', 'service']);
if (!$request->user()->hasRole('admin')) {
$query->where('service_id', $request->user()->service_id);
}
$query->when($request->search, function ($q, $search) {
$q->where(function ($sub) use ($search) {
$sub->where('titre', 'like', "%{$search}%")
->orWhereHas('fournisseur', fn($f) => $f->where('nom', 'like', "%{$search}%"));
});
})->when($request->service_id, function ($q, $serviceId) use ($request) {
if ($request->user()->hasRole('admin')) {
$q->where('service_id', $serviceId);
}
})->when($request->fournisseur_id, function ($q, $fournisseurId) {
$q->where('fournisseur_id', $fournisseurId);
})->when($request->statut, function ($q, $statut) {
$q->where('statut', $statut);
});
$contrats = $query->orderBy('date_echeance', 'asc')->paginate(20)->withQueryString();
// Calculate estProcheEcheance for the frontend
$contrats->getCollection()->transform(function ($contrat) {
$contrat->append(['est_proche_echeance', 'est_en_retard']);
return $contrat;
});
return Inertia::render('Contrats/Index', [
'contrats' => $contrats,
'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut']),
'statuts' => Contrat::STATUTS_LABELS,
]);
}
public function create(Request $request): Response
{
$this->authorize('create', Contrat::class);
return Inertia::render('Contrats/Create', [
'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'statuts' => Contrat::STATUTS_LABELS,
]);
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Contrat::class);
$rules = [
'titre' => 'required|string|max:255',
'description' => 'nullable|string',
'fournisseur_id' => 'required|exists:fournisseurs,id',
'date_debut' => 'nullable|date',
'date_echeance' => 'required|date',
'statut' => 'required|in:actif,a_renouveler,expire,resilie',
'montant' => 'nullable|numeric|min:0',
'preavis_jours' => 'nullable|integer|min:0',
];
// Only admins can select other services, otherwise we force the user's service
if ($request->user()->hasRole('admin')) {
$rules['service_id'] = 'required|exists:services,id';
}
$validated = $request->validate($rules);
if (!$request->user()->hasRole('admin')) {
$validated['service_id'] = $request->user()->service_id;
}
$contrat = Contrat::create($validated);
return redirect()->route('contrats.index')
->with('success', 'Contrat créé avec succès.');
}
public function show(Contrat $contrat): Response
{
$this->authorize('view', $contrat);
$contrat->load(['fournisseur', 'service', 'piecesJointes.user']);
$contrat->append(['est_proche_echeance', 'est_en_retard']);
return Inertia::render('Contrats/Show', [
'contrat' => $contrat,
]);
}
public function edit(Contrat $contrat, Request $request): Response
{
$this->authorize('update', $contrat);
return Inertia::render('Contrats/Edit', [
'contrat' => $contrat,
'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'statuts' => Contrat::STATUTS_LABELS,
]);
}
public function update(Request $request, Contrat $contrat): RedirectResponse
{
$this->authorize('update', $contrat);
$rules = [
'titre' => 'required|string|max:255',
'description' => 'nullable|string',
'fournisseur_id' => 'required|exists:fournisseurs,id',
'date_debut' => 'nullable|date',
'date_echeance' => 'required|date',
'statut' => 'required|in:actif,a_renouveler,expire,resilie',
'montant' => 'nullable|numeric|min:0',
'preavis_jours' => 'nullable|integer|min:0',
];
if ($request->user()->hasRole('admin')) {
$rules['service_id'] = 'required|exists:services,id';
}
$validated = $request->validate($rules);
$contrat->update($validated);
return redirect()->route('contrats.show', $contrat)
->with('success', 'Contrat mis à jour.');
}
public function destroy(Contrat $contrat): RedirectResponse
{
$this->authorize('delete', $contrat);
$contrat->delete();
return redirect()->route('contrats.index')
->with('success', 'Contrat supprimé.');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
use \Illuminate\Foundation\Auth\Access\AuthorizesRequests;
}

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers;
use App\Models\Commande;
use App\Models\Service;
use App\Models\Contrat;
use App\Models\Domaine;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function index(Request $request): Response
{
$user = $request->user();
$stats = [
'total' => Commande::count(),
'en_cours' => Commande::enCours()->count(),
'en_retard' => Commande::enRetard()->count(),
'brouillons' => Commande::parStatut('brouillon')->count(),
'en_attente_validation' => Commande::parStatut('en_attente_validation')->count(),
'validees' => Commande::parStatut('validee')->count(),
'commandees' => Commande::parStatut('commandee')->count(),
'partiellement_recues' => Commande::parStatut('partiellement_recue')->count(),
'recues_complete' => Commande::parStatut('recue_complete')->count(),
];
$commandesRecentes = Commande::with(['service', 'fournisseur', 'demandeur'])
->latest()
->limit(8)
->get();
$commandesEnRetard = Commande::enRetard()
->with(['service', 'fournisseur', 'demandeur'])
->orderBy('date_souhaitee')
->limit(10)
->get();
$commandesUrgentes = Commande::urgentes()
->enCours()
->with(['service', 'fournisseur', 'demandeur'])
->latest()
->limit(5)
->get();
$statsParStatut = Commande::select('statut', DB::raw('count(*) as total'))
->groupBy('statut')
->get()
->keyBy('statut');
$statsParService = Service::withCount([
'commandes',
'commandes as commandes_en_cours_count' => fn ($q) => $q->enCours(),
])->get();
$montantParMois = Commande::select(
DB::raw('YEAR(date_demande) as annee'),
DB::raw('MONTH(date_demande) as mois'),
DB::raw('SUM(montant_ttc) as total_ttc'),
DB::raw('COUNT(*) as nb_commandes')
)
->whereYear('date_demande', now()->year)
->whereNotIn('statut', ['annulee'])
->groupBy('annee', 'mois')
->orderBy('mois')
->get();
// Stats Contrats
$contratsQuery = Contrat::query();
if (!$user->hasRole('admin')) {
$contratsQuery->where('service_id', $user->service_id);
}
$tousContrats = $contratsQuery->get();
$statsContrats = [
'total' => $tousContrats->count(),
'proches' => $tousContrats->filter(fn($c) => $c->est_proche_echeance && !$c->est_en_retard)->count(),
'en_retard' => $tousContrats->filter(fn($c) => $c->est_en_retard)->count(),
];
// Stats Domaines (Visibles par tous)
$tousDomaines = Domaine::all();
$statsDomaines = [
'total' => $tousDomaines->count(),
'proches' => $tousDomaines->filter(fn($d) => $d->est_proche_echeance && !$d->est_en_retard)->count(),
'en_retard' => $tousDomaines->filter(fn($d) => $d->est_en_retard)->count(),
];
return Inertia::render('Dashboard/Index', compact(
'stats',
'commandesRecentes',
'commandesEnRetard',
'commandesUrgentes',
'statsParStatut',
'statsParService',
'montantParMois',
'statsContrats',
'statsDomaines',
));
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers;
use App\Models\Domaine;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Iodev\Whois\Factory as WhoisFactory;
use Iodev\Whois\Exceptions\ConnectionException;
use Iodev\Whois\Exceptions\ServerMismatchException;
use Iodev\Whois\Exceptions\WhoisException;
use Carbon\Carbon;
class DomaineController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('viewAny', Domaine::class);
$query = Domaine::query();
$query->when($request->search, function ($q, $search) {
$q->where('nom', 'like', "%{$search}%")
->orWhere('prestataire', 'like', "%{$search}%")
->orWhere('hebergeur', 'like', "%{$search}%");
});
$domaines = $query->orderBy('date_echeance', 'asc')->paginate(20)->withQueryString();
$domaines->getCollection()->transform(function ($domaine) {
$domaine->append(['est_proche_echeance', 'est_en_retard']);
return $domaine;
});
return Inertia::render('Domaines/Index', [
'domaines' => $domaines,
'filters' => $request->only(['search']),
]);
}
public function create(): Response
{
$this->authorize('create', Domaine::class);
return Inertia::render('Domaines/Create');
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Domaine::class);
$validated = $request->validate([
'nom' => 'required|string|max:255|unique:domaines,nom',
'date_echeance' => 'nullable|date',
'prestataire' => 'nullable|string',
'hebergeur' => 'nullable|string|max:255',
]);
$domaine = Domaine::create($validated);
// Si la date d'échéance n'est pas fournie, on tente de la récupérer via WHOIS
if (empty($validated['date_echeance'])) {
$this->syncWhoisDate($domaine);
}
return redirect()->route('domaines.index')
->with('success', 'Domaine créé avec succès.');
}
public function edit(Domaine $domaine): Response
{
$this->authorize('update', $domaine);
return Inertia::render('Domaines/Edit', [
'domaine' => $domaine,
]);
}
public function update(Request $request, Domaine $domaine): RedirectResponse
{
$this->authorize('update', $domaine);
$validated = $request->validate([
'nom' => 'required|string|max:255|unique:domaines,nom,' . $domaine->id,
'date_echeance' => 'nullable|date',
'prestataire' => 'nullable|string',
'hebergeur' => 'nullable|string|max:255',
]);
$domaine->update($validated);
return redirect()->route('domaines.index')
->with('success', 'Domaine mis à jour.');
}
public function destroy(Domaine $domaine): RedirectResponse
{
$this->authorize('delete', $domaine);
$domaine->delete();
return redirect()->route('domaines.index')
->with('success', 'Domaine supprimé.');
}
public function syncWhois(Domaine $domaine): RedirectResponse
{
$this->authorize('update', $domaine);
if ($this->syncWhoisDate($domaine)) {
return back()->with('success', 'Date d\'échéance synchronisée avec le Whois depuis le port 43.');
}
return back()->with('error', 'Impossible de récupérer la date d\'échéance pour ce domaine (Whois indisponible ou format inconnu).');
}
private function syncWhoisDate(Domaine $domaine): bool
{
try {
$whois = WhoisFactory::get()->createWhois();
$info = $whois->loadDomainInfo($domaine->nom);
if ($info && $info->expirationDate) {
// expirationDate is a unix timestamp integer in the package
$domaine->update([
'date_echeance' => Carbon::createFromTimestamp($info->expirationDate)->format('Y-m-d')
]);
return true;
}
} catch (ConnectionException $e) {
// Port 43 bloqué ou timeout
return false;
} catch (ServerMismatchException $e) {
// Serveur Whois inconnu
return false;
} catch (WhoisException $e) {
// Autre erreur Whois
return false;
} catch (\Exception $e) {
// Erreur inattendue
return false;
}
return false;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers;
use App\Models\Fournisseur;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class FournisseurController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('viewAny', Fournisseur::class);
$query = Fournisseur::withCount('commandes')
->when($request->search, fn ($q) => $q->where(function ($sub) use ($request) {
$sub->where('nom', 'like', "%{$request->search}%")
->orWhere('ville', 'like', "%{$request->search}%")
->orWhere('email', 'like', "%{$request->search}%");
}))
->when($request->has('active') && $request->active !== '', fn ($q) => $q->where('active', $request->active));
$fournisseurs = $query->orderBy('nom')->paginate(20)->withQueryString();
return Inertia::render('Fournisseurs/Index', [
'fournisseurs' => $fournisseurs,
'filters' => $request->only(['search', 'active']),
]);
}
public function create(): Response
{
$this->authorize('create', Fournisseur::class);
return Inertia::render('Fournisseurs/Create');
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Fournisseur::class);
$validated = $this->validateFournisseur($request);
Fournisseur::create($validated);
return redirect()->route('fournisseurs.index')
->with('success', 'Fournisseur créé avec succès.');
}
public function show(Fournisseur $fournisseur): Response
{
$this->authorize('view', $fournisseur);
$fournisseur->load(['commandes' => fn ($q) => $q->with('service', 'demandeur')->latest()->limit(10)]);
return Inertia::render('Fournisseurs/Show', compact('fournisseur'));
}
public function edit(Fournisseur $fournisseur): Response
{
$this->authorize('update', $fournisseur);
return Inertia::render('Fournisseurs/Edit', compact('fournisseur'));
}
public function update(Request $request, Fournisseur $fournisseur): RedirectResponse
{
$this->authorize('update', $fournisseur);
$validated = $this->validateFournisseur($request);
$fournisseur->update($validated);
return redirect()->route('fournisseurs.show', $fournisseur)
->with('success', 'Fournisseur mis à jour.');
}
public function destroy(Fournisseur $fournisseur): RedirectResponse
{
$this->authorize('delete', $fournisseur);
$fournisseur->delete();
return redirect()->route('fournisseurs.index')
->with('success', 'Fournisseur supprimé.');
}
public function toggleActive(Fournisseur $fournisseur): RedirectResponse
{
$this->authorize('update', $fournisseur);
$fournisseur->update(['active' => !$fournisseur->active]);
return back()->with('success', 'Statut du fournisseur mis à jour.');
}
private function validateFournisseur(Request $request): array
{
return $request->validate([
'nom' => 'required|string|max:255',
'raison_sociale' => 'nullable|string|max:255',
'siret' => 'nullable|string|max:14',
'adresse' => 'nullable|string',
'code_postal' => 'nullable|string|max:10',
'ville' => 'nullable|string|max:100',
'telephone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:255',
'contact_commercial' => 'nullable|string|max:255',
'email_commercial' => 'nullable|email|max:255',
'telephone_commercial' => 'nullable|string|max:20',
'site_web' => 'nullable|url|max:255',
'notes' => 'nullable|string',
'active' => 'boolean',
]);
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers;
use App\Models\Commande;
use App\Models\PieceJointe;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PieceJointeController extends Controller
{
/**
* Upload une pièce jointe sur une commande.
*/
public function store(Request $request, Commande $commande): RedirectResponse
{
$request->validate([
'type' => 'required|in:devis,bon_commande,bon_livraison,facture,autre',
'fichier' => [
'required',
'file',
'max:20480', // 20 Mo max
'mimes:pdf,jpg,jpeg,png,gif,webp,doc,docx,xls,xlsx,odt,ods,csv,zip',
],
'description' => 'nullable|string|max:500',
], [
'fichier.mimes' => 'Types autorisés : PDF, images, Word, Excel, OpenDocument, CSV, ZIP.',
'fichier.max' => 'La pièce jointe ne peut pas dépasser 20 Mo.',
]);
$fichier = $request->file('fichier');
$chemin = $fichier->store(
'commandes/' . $commande->id,
'private'
);
$commande->piecesJointes()->create([
'user_id' => $request->user()->id,
'type' => $request->type,
'nom_original' => $fichier->getClientOriginalName(),
'chemin' => $chemin,
'mime_type' => $fichier->getMimeType(),
'taille' => $fichier->getSize(),
'description' => $request->description,
]);
return back()->with('success', 'Pièce jointe ajoutée avec succès.');
}
/**
* Upload une pièce jointe sur un contrat.
*/
public function storeContrat(Request $request, \App\Models\Contrat $contrat): RedirectResponse
{
$request->validate([
'type' => 'required|in:contrat,avenant,autre',
'fichier' => [
'required',
'file',
'max:20480', // 20 Mo max
'mimes:pdf,jpg,jpeg,png,gif,webp,doc,docx,xls,xlsx,odt,ods,csv,zip',
],
'description' => 'nullable|string|max:500',
], [
'fichier.mimes' => 'Types autorisés : PDF, images, Word, Excel, OpenDocument, CSV, ZIP.',
'fichier.max' => 'La pièce jointe ne peut pas dépasser 20 Mo.',
]);
$fichier = $request->file('fichier');
$chemin = $fichier->store(
'contrats/' . $contrat->id,
'private'
);
$contrat->piecesJointes()->create([
'user_id' => $request->user()->id,
'type' => $request->type,
'nom_original' => $fichier->getClientOriginalName(),
'chemin' => $chemin,
'mime_type' => $fichier->getMimeType(),
'taille' => $fichier->getSize(),
'description' => $request->description,
]);
return back()->with('success', 'Pièce jointe ajoutée avec succès au contrat.');
}
/**
* Télécharger une pièce jointe (accès sécurisé, sans URL publique).
*/
public function download(PieceJointe $pieceJointe): StreamedResponse
{
// Vérifier que l'utilisateur est authentifié (middleware auth déjà sur le groupe)
abort_unless(Storage::disk('private')->exists($pieceJointe->chemin), 404);
return Storage::disk('private')->download(
$pieceJointe->chemin,
$pieceJointe->nom_original
);
}
/**
* Supprimer une pièce jointe.
*/
public function destroy(PieceJointe $pieceJointe): RedirectResponse
{
$this->authorize('delete', $pieceJointe);
$pieceJointe->supprimer();
return back()->with('success', 'Pièce jointe supprimée.');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\Service;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ServiceController extends Controller
{
public function index(): Response
{
return Inertia::render('Services/Index', [
'services' => Service::withCount('users', 'commandes')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'nom' => 'required|string|max:100|unique:services,nom',
'description' => 'nullable|string',
'couleur' => 'nullable|string|max:7',
'icone' => 'nullable|string|max:50',
]);
Service::create($validated);
return back()->with('success', 'Service créé.');
}
public function update(Request $request, Service $service): RedirectResponse
{
$validated = $request->validate([
'nom' => 'required|string|max:100|unique:services,nom,' . $service->id,
'description' => 'nullable|string',
'couleur' => 'nullable|string|max:7',
'icone' => 'nullable|string|max:50',
]);
$service->update($validated);
return back()->with('success', 'Service mis à jour.');
}
public function destroy(Service $service): RedirectResponse
{
if ($service->users()->exists() || $service->commandes()->exists()) {
return back()->with('error', 'Impossible de supprimer un service ayant des utilisateurs ou des commandes.');
}
$service->delete();
return back()->with('success', 'Service supprimé.');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Models\Service;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
public function index(): Response
{
$this->authorize('viewAny', User::class);
return Inertia::render('Users/Index', [
'users' => User::with('service', 'roles')->orderBy('name')->paginate(20),
'services' => Service::all(),
'roles' => Role::all(),
]);
}
public function edit(User $user): Response
{
$this->authorize('update', $user);
return Inertia::render('Users/Edit', [
'user' => $user->load('service', 'roles'),
'services' => Service::all(),
'roles' => Role::all(),
]);
}
public function update(Request $request, User $user): RedirectResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $user->id,
'service_id' => 'nullable|exists:services,id',
'telephone' => 'nullable|string|max:20',
'role' => 'required|string|exists:roles,name',
'password' => 'nullable|string|min:8|confirmed',
]);
$user->update([
'name' => $validated['name'],
'email' => $validated['email'],
'service_id' => $validated['service_id'],
'telephone' => $validated['telephone'],
...(isset($validated['password']) ? ['password' => Hash::make($validated['password'])] : []),
]);
$user->syncRoles([$validated['role']]);
return redirect()->route('users.index')
->with('success', 'Utilisateur mis à jour.');
}
public function toggleActive(User $user): RedirectResponse
{
$this->authorize('update', $user);
$user->update(['active' => !$user->active]);
return back()->with('success', 'Statut de l\'utilisateur mis à jour.');
}
}