Initial commit with contrats and domaines modules
This commit is contained in:
94
app/Http/Controllers/ArticleController.php
Normal file
94
app/Http/Controllers/ArticleController.php
Normal 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é.');
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
52
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal 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('/');
|
||||
}
|
||||
}
|
||||
41
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
41
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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')]);
|
||||
}
|
||||
}
|
||||
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
69
app/Http/Controllers/Auth/NewPasswordController.php
Normal 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)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
29
app/Http/Controllers/Auth/PasswordController.php
Normal file
29
app/Http/Controllers/Auth/PasswordController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
51
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal 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)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
52
app/Http/Controllers/Auth/RegisteredUserController.php
Normal 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));
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/CategorieController.php
Normal file
63
app/Http/Controllers/CategorieController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
247
app/Http/Controllers/CommandeController.php
Normal file
247
app/Http/Controllers/CommandeController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
161
app/Http/Controllers/ContratController.php
Normal file
161
app/Http/Controllers/ContratController.php
Normal 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é.');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
use \Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
}
|
||||
103
app/Http/Controllers/DashboardController.php
Normal file
103
app/Http/Controllers/DashboardController.php
Normal 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',
|
||||
));
|
||||
}
|
||||
}
|
||||
148
app/Http/Controllers/DomaineController.php
Normal file
148
app/Http/Controllers/DomaineController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
116
app/Http/Controllers/FournisseurController.php
Normal file
116
app/Http/Controllers/FournisseurController.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
115
app/Http/Controllers/PieceJointeController.php
Normal file
115
app/Http/Controllers/PieceJointeController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/ProfileController.php
Normal file
63
app/Http/Controllers/ProfileController.php
Normal 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('/');
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/ServiceController.php
Normal file
58
app/Http/Controllers/ServiceController.php
Normal 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é.');
|
||||
}
|
||||
}
|
||||
73
app/Http/Controllers/UserController.php
Normal file
73
app/Http/Controllers/UserController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
36
app/Http/Middleware/HandleInertiaRequests.php
Normal file
36
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Commande;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
protected $rootView = 'app';
|
||||
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'auth' => [
|
||||
'user' => $request->user()?->load('service', 'roles'),
|
||||
],
|
||||
'flash' => [
|
||||
'success' => fn () => $request->session()->get('success'),
|
||||
'error' => fn () => $request->session()->get('error'),
|
||||
],
|
||||
'config' => [
|
||||
'statuts' => Commande::STATUTS_LABELS,
|
||||
'priorites' => Commande::PRIORITES_LABELS,
|
||||
'statuts_couleurs' => Commande::STATUTS_COULEURS,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
86
app/Http/Requests/Auth/LoginRequest.php
Normal file
86
app/Http/Requests/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest 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, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/ProfileUpdateRequest.php
Normal file
31
app/Http/Requests/ProfileUpdateRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Models/Article.php
Normal file
35
app/Models/Article.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class Article extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'reference', 'designation', 'description',
|
||||
'categorie_id', 'fournisseur_id', 'prix_unitaire_ht', 'unite', 'active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'prix_unitaire_ht' => 'decimal:2',
|
||||
'active' => 'boolean',
|
||||
];
|
||||
|
||||
public function categorie(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Categorie::class);
|
||||
}
|
||||
|
||||
public function fournisseur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Fournisseur::class);
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('active', true);
|
||||
}
|
||||
}
|
||||
32
app/Models/Categorie.php
Normal file
32
app/Models/Categorie.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class Categorie extends Model
|
||||
{
|
||||
protected $fillable = ['nom', 'description', 'couleur', 'icone', 'ordre', 'active'];
|
||||
|
||||
protected $casts = [
|
||||
'active' => 'boolean',
|
||||
'ordre' => 'integer',
|
||||
];
|
||||
|
||||
public function articles(): HasMany
|
||||
{
|
||||
return $this->hasMany(Article::class);
|
||||
}
|
||||
|
||||
public function lignesCommande(): HasMany
|
||||
{
|
||||
return $this->hasMany(LigneCommande::class);
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('active', true);
|
||||
}
|
||||
}
|
||||
267
app/Models/Commande.php
Normal file
267
app/Models/Commande.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Commande extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'numero_commande', 'service_id', 'fournisseur_id', 'user_id',
|
||||
'validateur_id', 'acheteur_id', 'objet', 'description', 'justification',
|
||||
'statut', 'priorite', 'reference_fournisseur', 'imputation_budgetaire',
|
||||
'montant_ht', 'montant_ttc',
|
||||
'date_demande', 'date_souhaitee', 'date_validation', 'date_commande',
|
||||
'date_livraison_prevue', 'date_reception', 'date_reception_complete', 'date_cloture',
|
||||
'notes', 'notes_fournisseur',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'montant_ht' => 'decimal:2',
|
||||
'montant_ttc' => 'decimal:2',
|
||||
'date_demande' => 'date',
|
||||
'date_souhaitee' => 'date',
|
||||
'date_validation' => 'datetime',
|
||||
'date_commande' => 'datetime',
|
||||
'date_livraison_prevue' => 'date',
|
||||
'date_reception' => 'datetime',
|
||||
'date_reception_complete' => 'datetime',
|
||||
'date_cloture' => 'datetime',
|
||||
];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Constantes
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
const STATUTS_LABELS = [
|
||||
'brouillon' => 'Brouillon',
|
||||
'en_attente_validation' => 'En attente de validation',
|
||||
'validee' => 'Validée',
|
||||
'commandee' => 'Commandée',
|
||||
'partiellement_recue' => 'Partiellement reçue',
|
||||
'recue_complete' => 'Reçue complète',
|
||||
'cloturee' => 'Clôturée',
|
||||
'annulee' => 'Annulée',
|
||||
];
|
||||
|
||||
const STATUTS_COULEURS = [
|
||||
'brouillon' => 'gray',
|
||||
'en_attente_validation' => 'yellow',
|
||||
'validee' => 'blue',
|
||||
'commandee' => 'indigo',
|
||||
'partiellement_recue' => 'orange',
|
||||
'recue_complete' => 'green',
|
||||
'cloturee' => 'slate',
|
||||
'annulee' => 'red',
|
||||
];
|
||||
|
||||
const PRIORITES_LABELS = [
|
||||
'normale' => 'Normale',
|
||||
'haute' => 'Haute',
|
||||
'urgente' => 'Urgente',
|
||||
];
|
||||
|
||||
const STATUT_TRANSITIONS = [
|
||||
'brouillon' => ['en_attente_validation', 'annulee'],
|
||||
'en_attente_validation' => ['validee', 'brouillon', 'annulee'],
|
||||
'validee' => ['commandee', 'annulee'],
|
||||
'commandee' => ['partiellement_recue', 'recue_complete', 'annulee'],
|
||||
'partiellement_recue' => ['recue_complete', 'annulee'],
|
||||
'recue_complete' => ['cloturee'],
|
||||
'cloturee' => [],
|
||||
'annulee' => [],
|
||||
];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Relations
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function service(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
}
|
||||
|
||||
public function fournisseur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Fournisseur::class);
|
||||
}
|
||||
|
||||
public function demandeur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function validateur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'validateur_id');
|
||||
}
|
||||
|
||||
public function acheteur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'acheteur_id');
|
||||
}
|
||||
|
||||
public function lignes(): HasMany
|
||||
{
|
||||
return $this->hasMany(LigneCommande::class)->orderBy('ordre');
|
||||
}
|
||||
|
||||
public function historique(): HasMany
|
||||
{
|
||||
return $this->hasMany(HistoriqueCommande::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function piecesJointes(): HasMany
|
||||
{
|
||||
return $this->hasMany(PieceJointe::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Scopes
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function scopeEnCours(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNotIn('statut', ['cloturee', 'annulee']);
|
||||
}
|
||||
|
||||
public function scopeEnRetard(Builder $query): Builder
|
||||
{
|
||||
return $query->enCours()->whereNotNull('date_souhaitee')->where('date_souhaitee', '<', now()->toDateString());
|
||||
}
|
||||
|
||||
public function scopeUrgentes(Builder $query): Builder
|
||||
{
|
||||
return $query->where('priorite', 'urgente');
|
||||
}
|
||||
|
||||
public function scopeParService(Builder $query, int $serviceId): Builder
|
||||
{
|
||||
return $query->where('service_id', $serviceId);
|
||||
}
|
||||
|
||||
public function scopeParStatut(Builder $query, string $statut): Builder
|
||||
{
|
||||
return $query->where('statut', $statut);
|
||||
}
|
||||
|
||||
public function scopeParFournisseur(Builder $query, int $fournisseurId): Builder
|
||||
{
|
||||
return $query->where('fournisseur_id', $fournisseurId);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Accesseurs
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function getStatutLabelAttribute(): string
|
||||
{
|
||||
return self::STATUTS_LABELS[$this->statut] ?? $this->statut;
|
||||
}
|
||||
|
||||
public function getStatutCouleurAttribute(): string
|
||||
{
|
||||
return self::STATUTS_COULEURS[$this->statut] ?? 'gray';
|
||||
}
|
||||
|
||||
public function getEstEnRetardAttribute(): bool
|
||||
{
|
||||
return !in_array($this->statut, ['cloturee', 'annulee'])
|
||||
&& $this->date_souhaitee !== null
|
||||
&& $this->date_souhaitee->isPast();
|
||||
}
|
||||
|
||||
public function getTauxReceptionAttribute(): float
|
||||
{
|
||||
$total = $this->lignes->sum(fn ($l) => (float) $l->quantite);
|
||||
if ($total <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
$recu = $this->lignes->sum(fn ($l) => (float) $l->quantite_recue);
|
||||
|
||||
return round(min($recu / $total * 100, 100), 1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Numérotation automatique
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public static function genererNumero(): string
|
||||
{
|
||||
$annee = now()->year;
|
||||
$derniere = static::withTrashed()
|
||||
->where('numero_commande', 'like', "CMD-IT-{$annee}-%")
|
||||
->orderByDesc('id')
|
||||
->value('numero_commande');
|
||||
|
||||
$sequence = 1;
|
||||
if ($derniere) {
|
||||
$sequence = (int) substr($derniere, strrpos($derniere, '-') + 1) + 1;
|
||||
}
|
||||
|
||||
return sprintf('CMD-IT-%d-%04d', $annee, $sequence);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Moteur de transitions
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function peutTransitionnerVers(string $statut): bool
|
||||
{
|
||||
return in_array($statut, self::STATUT_TRANSITIONS[$this->statut] ?? []);
|
||||
}
|
||||
|
||||
public function transitionnerVers(string $statut, User $user, ?string $commentaire = null): bool
|
||||
{
|
||||
if (!$this->peutTransitionnerVers($statut)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ancienStatut = $this->statut;
|
||||
$this->statut = $statut;
|
||||
|
||||
// Mettre à jour les dates et les acteurs selon le nouveau statut
|
||||
match ($statut) {
|
||||
'validee' => $this->fill(['date_validation' => now(), 'validateur_id' => $user->id]),
|
||||
'commandee' => $this->fill(['date_commande' => now(), 'acheteur_id' => $user->id]),
|
||||
'partiellement_recue' => $this->fill(['date_reception' => $this->date_reception ?? now()]),
|
||||
'recue_complete' => $this->fill(['date_reception_complete' => now(), 'date_reception' => $this->date_reception ?? now()]),
|
||||
'cloturee' => $this->fill(['date_cloture' => now()]),
|
||||
default => null,
|
||||
};
|
||||
|
||||
$this->save();
|
||||
|
||||
HistoriqueCommande::create([
|
||||
'commande_id' => $this->id,
|
||||
'user_id' => $user->id,
|
||||
'ancien_statut' => $ancienStatut,
|
||||
'nouveau_statut' => $statut,
|
||||
'commentaire' => $commentaire,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Recalcul des montants (appelé depuis LigneCommande)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function recalculerMontants(): void
|
||||
{
|
||||
$this->lignes()->get()->tap(function ($lignes) {
|
||||
$ht = $lignes->sum(fn ($l) => (float) $l->montant_ht);
|
||||
$ttc = $lignes->sum(fn ($l) => (float) $l->montant_ttc);
|
||||
$this->withoutEvents(function () use ($ht, $ttc) {
|
||||
$this->update(['montant_ht' => $ht, 'montant_ttc' => $ttc]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
75
app/Models/Contrat.php
Normal file
75
app/Models/Contrat.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Contrat extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'titre',
|
||||
'description',
|
||||
'fournisseur_id',
|
||||
'service_id',
|
||||
'date_debut',
|
||||
'date_echeance',
|
||||
'statut',
|
||||
'montant',
|
||||
'preavis_jours',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date_debut' => 'date',
|
||||
'date_echeance' => 'date',
|
||||
'montant' => 'decimal:2',
|
||||
];
|
||||
|
||||
const STATUTS_LABELS = [
|
||||
'actif' => 'Actif',
|
||||
'a_renouveler' => 'À renouveler',
|
||||
'expire' => 'Expiré',
|
||||
'resilie' => 'Résilié',
|
||||
];
|
||||
|
||||
public function fournisseur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Fournisseur::class);
|
||||
}
|
||||
|
||||
public function service(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
}
|
||||
|
||||
public function piecesJointes(): HasMany
|
||||
{
|
||||
return $this->hasMany(PieceJointe::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
// Un contrat est considéré "proche d'expiration" si l'échéance est dans moins de 30 jours (ou selon son préavis)
|
||||
public function getEstProcheEcheanceAttribute(): bool
|
||||
{
|
||||
if (!$this->date_echeance || in_array($this->statut, ['resilie', 'expire'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$joursAvantAlerte = $this->preavis_jours > 0 ? $this->preavis_jours + 15 : 30;
|
||||
|
||||
return Carbon::now()->diffInDays($this->date_echeance, false) <= $joursAvantAlerte;
|
||||
}
|
||||
|
||||
public function getEstEnRetardAttribute(): bool
|
||||
{
|
||||
if (!$this->date_echeance || $this->statut === 'resilie') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Carbon::now()->isAfter($this->date_echeance);
|
||||
}
|
||||
}
|
||||
42
app/Models/Domaine.php
Normal file
42
app/Models/Domaine.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Domaine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'nom',
|
||||
'date_echeance',
|
||||
'prestataire',
|
||||
'hebergeur',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date_echeance' => 'date',
|
||||
];
|
||||
|
||||
// Un domaine est considéré "proche d'expiration" si l'échéance est dans moins de 30 jours
|
||||
public function getEstProcheEcheanceAttribute(): bool
|
||||
{
|
||||
if (!$this->date_echeance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Carbon::now()->diffInDays($this->date_echeance, false) <= 30;
|
||||
}
|
||||
|
||||
public function getEstEnRetardAttribute(): bool
|
||||
{
|
||||
if (!$this->date_echeance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Carbon::now()->isAfter($this->date_echeance);
|
||||
}
|
||||
}
|
||||
35
app/Models/Fournisseur.php
Normal file
35
app/Models/Fournisseur.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class Fournisseur extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'nom', 'raison_sociale', 'siret', 'adresse', 'code_postal', 'ville',
|
||||
'telephone', 'email', 'contact_commercial', 'email_commercial',
|
||||
'telephone_commercial', 'site_web', 'notes', 'active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'active' => 'boolean',
|
||||
];
|
||||
|
||||
public function commandes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Commande::class);
|
||||
}
|
||||
|
||||
public function articles(): HasMany
|
||||
{
|
||||
return $this->hasMany(Article::class);
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('active', true);
|
||||
}
|
||||
}
|
||||
27
app/Models/HistoriqueCommande.php
Normal file
27
app/Models/HistoriqueCommande.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class HistoriqueCommande extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['commande_id', 'user_id', 'ancien_statut', 'nouveau_statut', 'commentaire'];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function commande(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Commande::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
60
app/Models/LigneCommande.php
Normal file
60
app/Models/LigneCommande.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class LigneCommande extends Model
|
||||
{
|
||||
protected $table = 'lignes_commande';
|
||||
|
||||
protected $fillable = [
|
||||
'commande_id', 'article_id', 'categorie_id',
|
||||
'designation', 'reference', 'quantite', 'quantite_recue',
|
||||
'unite', 'prix_unitaire_ht', 'taux_tva',
|
||||
'montant_ht', 'montant_ttc', 'notes', 'ordre',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantite' => 'decimal:3',
|
||||
'quantite_recue' => 'decimal:3',
|
||||
'prix_unitaire_ht' => 'decimal:2',
|
||||
'taux_tva' => 'decimal:2',
|
||||
'montant_ht' => 'decimal:2',
|
||||
'montant_ttc' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (LigneCommande $ligne) {
|
||||
if ($ligne->prix_unitaire_ht !== null && $ligne->quantite !== null) {
|
||||
$ligne->montant_ht = round((float) $ligne->quantite * (float) $ligne->prix_unitaire_ht, 2);
|
||||
$ligne->montant_ttc = round((float) $ligne->montant_ht * (1 + (float) $ligne->taux_tva / 100), 2);
|
||||
}
|
||||
});
|
||||
|
||||
static::saved(function (LigneCommande $ligne) {
|
||||
$ligne->commande?->recalculerMontants();
|
||||
});
|
||||
|
||||
static::deleted(function (LigneCommande $ligne) {
|
||||
$ligne->commande?->recalculerMontants();
|
||||
});
|
||||
}
|
||||
|
||||
public function commande(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Commande::class);
|
||||
}
|
||||
|
||||
public function article(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Article::class);
|
||||
}
|
||||
|
||||
public function categorie(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Categorie::class);
|
||||
}
|
||||
}
|
||||
113
app/Models/PieceJointe.php
Normal file
113
app/Models/PieceJointe.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class PieceJointe extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $table = 'pieces_jointes';
|
||||
|
||||
protected $fillable = [
|
||||
'commande_id', 'contrat_id', 'user_id', 'type',
|
||||
'nom_original', 'chemin', 'mime_type', 'taille', 'description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'taille' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
const TYPES_LABELS = [
|
||||
'devis' => 'Devis',
|
||||
'bon_commande' => 'Bon de commande',
|
||||
'bon_livraison' => 'Bon de livraison',
|
||||
'facture' => 'Facture',
|
||||
'contrat' => 'Contrat',
|
||||
'avenant' => 'Avenant',
|
||||
'autre' => 'Autre',
|
||||
];
|
||||
|
||||
const TYPES_ICONES = [
|
||||
'devis' => '📋',
|
||||
'bon_commande' => '🛒',
|
||||
'bon_livraison' => '📦',
|
||||
'facture' => '🧾',
|
||||
'contrat' => '📄',
|
||||
'avenant' => '📑',
|
||||
'autre' => '📎',
|
||||
];
|
||||
|
||||
const TYPES_COULEURS = [
|
||||
'devis' => 'purple',
|
||||
'bon_commande' => 'blue',
|
||||
'bon_livraison' => 'orange',
|
||||
'facture' => 'green',
|
||||
'contrat' => 'indigo',
|
||||
'avenant' => 'teal',
|
||||
'autre' => 'gray',
|
||||
];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Relations
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function commande(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Commande::class);
|
||||
}
|
||||
|
||||
public function contrat(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contrat::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Accesseurs
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function getTypeLabelAttribute(): string
|
||||
{
|
||||
return self::TYPES_LABELS[$this->type] ?? $this->type;
|
||||
}
|
||||
|
||||
public function getTailleFormatteeAttribute(): string
|
||||
{
|
||||
$taille = $this->taille;
|
||||
if ($taille < 1024) {
|
||||
return $taille . ' o';
|
||||
} elseif ($taille < 1024 * 1024) {
|
||||
return round($taille / 1024, 1) . ' Ko';
|
||||
}
|
||||
return round($taille / (1024 * 1024), 1) . ' Mo';
|
||||
}
|
||||
|
||||
public function getEstImageAttribute(): bool
|
||||
{
|
||||
return str_starts_with($this->mime_type, 'image/');
|
||||
}
|
||||
|
||||
public function getEstPdfAttribute(): bool
|
||||
{
|
||||
return $this->mime_type === 'application/pdf';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Méthodes
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public function supprimer(): void
|
||||
{
|
||||
Storage::disk('private')->delete($this->chemin);
|
||||
$this->delete();
|
||||
}
|
||||
}
|
||||
21
app/Models/Service.php
Normal file
21
app/Models/Service.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Service extends Model
|
||||
{
|
||||
protected $fillable = ['nom', 'description', 'couleur', 'icone'];
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function commandes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Commande::class);
|
||||
}
|
||||
}
|
||||
48
app/Models/User.php
Normal file
48
app/Models/User.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable, HasRoles;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'email', 'password', 'service_id', 'telephone', 'active',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password', 'remember_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function service(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
}
|
||||
|
||||
public function commandesCreees(): HasMany
|
||||
{
|
||||
return $this->hasMany(Commande::class, 'user_id');
|
||||
}
|
||||
|
||||
public function commandesValidees(): HasMany
|
||||
{
|
||||
return $this->hasMany(Commande::class, 'validateur_id');
|
||||
}
|
||||
}
|
||||
68
app/Policies/CommandePolicy.php
Normal file
68
app/Policies/CommandePolicy.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Commande;
|
||||
use App\Models\User;
|
||||
|
||||
class CommandePolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view(User $user, Commande $commande): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['admin', 'responsable', 'acheteur']);
|
||||
}
|
||||
|
||||
public function update(User $user, Commande $commande): bool
|
||||
{
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$statutsEditables = ['brouillon', 'en_attente_validation'];
|
||||
if (!in_array($commande->statut, $statutsEditables)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasAnyRole(['responsable', 'acheteur'])
|
||||
&& ($commande->user_id === $user->id || $commande->service_id === $user->service_id);
|
||||
}
|
||||
|
||||
public function delete(User $user, Commande $commande): bool
|
||||
{
|
||||
return $user->hasRole('admin');
|
||||
}
|
||||
|
||||
public function transition(User $user, Commande $commande, string $targetStatut): bool
|
||||
{
|
||||
if (!$commande->peutTransitionnerVers($targetStatut)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return match ($targetStatut) {
|
||||
'en_attente_validation' => $user->hasAnyRole(['responsable', 'acheteur']) && $commande->user_id === $user->id,
|
||||
'brouillon' => $user->hasAnyRole(['responsable', 'acheteur']) && $commande->user_id === $user->id,
|
||||
'validee' => $user->hasRole('responsable') && $commande->service_id === $user->service_id,
|
||||
'commandee' => $user->hasAnyRole(['responsable', 'acheteur']),
|
||||
'partiellement_recue',
|
||||
'recue_complete' => $user->hasAnyRole(['responsable', 'acheteur']),
|
||||
'cloturee' => $user->hasAnyRole(['responsable', 'acheteur']),
|
||||
'annulee' => $user->hasAnyRole(['responsable', 'acheteur'])
|
||||
&& !in_array($commande->statut, ['commandee', 'partiellement_recue', 'recue_complete', 'cloturee']),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
46
app/Policies/ContratPolicy.php
Normal file
46
app/Policies/ContratPolicy.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Contrat;
|
||||
use App\Models\User;
|
||||
|
||||
class ContratPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view(User $user, Contrat $contrat): bool
|
||||
{
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->service_id === $contrat->service_id;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true; // Anyone can create, but in the controller we'll restrict to their own service
|
||||
}
|
||||
|
||||
public function update(User $user, Contrat $contrat): bool
|
||||
{
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->service_id === $contrat->service_id;
|
||||
}
|
||||
|
||||
public function delete(User $user, Contrat $contrat): bool
|
||||
{
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->service_id === $contrat->service_id;
|
||||
}
|
||||
}
|
||||
44
app/Policies/DomainePolicy.php
Normal file
44
app/Policies/DomainePolicy.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Domaine;
|
||||
use App\Models\User;
|
||||
|
||||
class DomainePolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view(User $user, Domaine $domaine): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function canManage(User $user): bool
|
||||
{
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must belong to "Infrastructure" service
|
||||
return $user->service && $user->service->nom === 'Infrastructure';
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function update(User $user, Domaine $domaine): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function delete(User $user, Domaine $domaine): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
}
|
||||
34
app/Policies/FournisseurPolicy.php
Normal file
34
app/Policies/FournisseurPolicy.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Fournisseur;
|
||||
use App\Models\User;
|
||||
|
||||
class FournisseurPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view(User $user, Fournisseur $fournisseur): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['admin', 'responsable', 'acheteur']);
|
||||
}
|
||||
|
||||
public function update(User $user, Fournisseur $fournisseur): bool
|
||||
{
|
||||
return $user->hasAnyRole(['admin', 'responsable', 'acheteur']);
|
||||
}
|
||||
|
||||
public function delete(User $user, Fournisseur $fournisseur): bool
|
||||
{
|
||||
return $user->hasRole('admin');
|
||||
}
|
||||
}
|
||||
15
app/Policies/PieceJointePolicy.php
Normal file
15
app/Policies/PieceJointePolicy.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\PieceJointe;
|
||||
use App\Models\User;
|
||||
|
||||
class PieceJointePolicy
|
||||
{
|
||||
public function delete(User $user, PieceJointe $pieceJointe): bool
|
||||
{
|
||||
// Admin ou celui qui a uploadé
|
||||
return $user->hasRole('admin') || $pieceJointe->user_id === $user->id;
|
||||
}
|
||||
}
|
||||
23
app/Policies/UserPolicy.php
Normal file
23
app/Policies/UserPolicy.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasRole('admin');
|
||||
}
|
||||
|
||||
public function update(User $user, User $model): bool
|
||||
{
|
||||
return $user->hasRole('admin');
|
||||
}
|
||||
|
||||
public function delete(User $user, User $model): bool
|
||||
{
|
||||
return $user->hasRole('admin') && $user->id !== $model->id;
|
||||
}
|
||||
}
|
||||
48
app/Providers/AppServiceProvider.php
Normal file
48
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Categorie;
|
||||
use App\Models\Commande;
|
||||
use App\Models\Fournisseur;
|
||||
use App\Models\PieceJointe;
|
||||
use App\Models\User;
|
||||
use App\Policies\CommandePolicy;
|
||||
use App\Policies\FournisseurPolicy;
|
||||
use App\Policies\PieceJointePolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $policies = [
|
||||
Commande::class => CommandePolicy::class,
|
||||
Fournisseur::class => FournisseurPolicy::class,
|
||||
PieceJointe::class => PieceJointePolicy::class,
|
||||
User::class => UserPolicy::class,
|
||||
];
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerPolicies();
|
||||
Vite::prefetch(concurrency: 3);
|
||||
|
||||
// Les admins passent toutes les gates sauf les policies de user
|
||||
Gate::before(function (User $user, string $ability) {
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Gates pour catégories et services (pas de modèle dédié à une Policy)
|
||||
Gate::define('manage-categories', fn (User $user) => $user->hasAnyRole(['admin', 'responsable']));
|
||||
Gate::define('manage-services', fn (User $user) => $user->hasRole('admin'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user