feat: infrastructure assets management with warranty tracking and EAN lookup integration

This commit is contained in:
jeremy bayse
2026-04-09 21:51:43 +02:00
parent 3544c77bd1
commit b28c56c94c
46 changed files with 2961 additions and 414 deletions

View File

@@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers;
use App\Models\Asset;
use App\Models\Commune;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class AssetController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('viewAny', Asset::class);
$query = Asset::with('commune');
if ($request->search) {
$query->where(function ($q) use ($request) {
$q->where('nom', 'like', "%{$request->search}%")
->orWhere('numero_serie', 'like', "%{$request->search}%")
->orWhere('type', 'like', "%{$request->search}%");
});
}
if ($request->type) {
$query->where('type', $request->type);
}
if ($request->commune_id) {
$query->where('commune_id', $request->commune_id);
}
$assets = $query->latest()->paginate(20)->withQueryString();
return Inertia::render('Assets/Index', [
'assets' => $assets,
'filters' => $request->only(['search', 'type', 'commune_id']),
'communes' => Commune::orderBy('nom')->get(),
'types' => Asset::distinct()->pluck('type'),
]);
}
public function create(): Response
{
$this->authorize('create', Asset::class);
return Inertia::render('Assets/Form', [
'communes' => Commune::orderBy('nom')->get(),
'commandes' => \App\Models\Commande::select('id', 'numero_commande', 'objet')->latest()->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Asset::class);
$validated = $request->validate([
'nom' => 'required|string|max:255',
'type' => 'required|string|max:100',
'code_ean' => 'nullable|string|max:50',
'marque' => 'nullable|string|max:100',
'modele' => 'nullable|string|max:100',
'numero_serie' => 'nullable|string|max:100',
'emplacement' => 'nullable|string|max:255',
'commune_id' => 'nullable|exists:communes,id',
'commande_id' => 'nullable|exists:commandes,id',
'date_achat' => 'nullable|date',
'date_fin_garantie' => 'nullable|date',
'statut' => 'required|in:en_service,hors_service,en_reparation,stock',
'notes' => 'nullable|string',
]);
Asset::create($validated);
return redirect()->route('assets.index')
->with('success', 'Asset ajouté avec succès.');
}
public function show(Asset $asset): Response
{
$this->authorize('view', $asset);
$asset->load(['commune', 'commande']);
return Inertia::render('Assets/Show', [
'asset' => $asset,
]);
}
public function edit(Asset $asset): Response
{
$this->authorize('update', $asset);
return Inertia::render('Assets/Form', [
'asset' => $asset,
'communes' => Commune::orderBy('nom')->get(),
'commandes' => \App\Models\Commande::select('id', 'numero_commande', 'objet')->latest()->get(),
]);
}
public function update(Request $request, Asset $asset): RedirectResponse
{
$this->authorize('update', $asset);
$validated = $request->validate([
'nom' => 'required|string|max:255',
'type' => 'required|string|max:100',
'code_ean' => 'nullable|string|max:50',
'marque' => 'nullable|string|max:100',
'modele' => 'nullable|string|max:100',
'numero_serie' => 'nullable|string|max:100',
'emplacement' => 'nullable|string|max:255',
'commune_id' => 'nullable|exists:communes,id',
'commande_id' => 'nullable|exists:commandes,id',
'date_achat' => 'nullable|date',
'date_fin_garantie' => 'nullable|date',
'statut' => 'required|in:en_service,hors_service,en_reparation,stock',
'notes' => 'nullable|string',
]);
$asset->update($validated);
return redirect()->route('assets.index')
->with('success', 'Asset mis à jour.');
}
public function destroy(Asset $asset): RedirectResponse
{
$this->authorize('delete', $asset);
$asset->delete();
return redirect()->route('assets.index')
->with('success', 'Asset supprimé.');
}
public function lookupEan($ean)
{
$response = \Illuminate\Support\Facades\Http::get("https://api.upcitemdb.com/prod/trial/lookup", [
'upc' => $ean
]);
return response()->json($response->json(), $response->status());
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers;
use App\Models\Contrat;
use App\Models\Licence;
use App\Models\Domaine;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class CalendarController extends Controller
{
public function index(): Response
{
return Inertia::render('Calendar/Index');
}
/**
* Fetch events for FullCalendar.
*/
public function events(Request $request)
{
$events = [];
// 1. Contracts expirations
$contrats = Contrat::with(['fournisseur', 'service'])
->whereNotNull('date_echeance')
->get();
foreach ($contrats as $contrat) {
$events[] = [
'id' => 'contrat-' . $contrat->id,
'title' => '📑 ' . $contrat->titre . ' (' . ($contrat->fournisseur->nom ?? 'N/A') . ')',
'start' => $contrat->date_echeance->toDateString(),
'url' => route('contrats.show', $contrat->id),
'backgroundColor' => $this->getContratColor($contrat),
'extendedProps' => [
'type' => 'Contrat',
'service' => $contrat->service->nom ?? 'N/A',
]
];
}
// 2. Licenses expirations
$licences = Licence::with(['fournisseur'])
->whereNotNull('date_expiration')
->get();
foreach ($licences as $licence) {
$events[] = [
'id' => 'licence-' . $licence->id,
'title' => '🔑 ' . $licence->nom . ' (' . ($licence->fournisseur->nom ?? 'N/A') . ')',
'start' => $licence->date_expiration->toDateString(),
'url' => route('licences.index'), // Link to index or show if implemented
'backgroundColor' => '#3498db',
'extendedProps' => [
'type' => 'Licence',
'usage' => $licence->nombre_sieges_utilises . '/' . $licence->nombre_sieges_total,
]
];
}
// 3. Domain expirations
$domaines = Domaine::whereNotNull('date_echeance')->get();
foreach ($domaines as $domaine) {
$events[] = [
'id' => 'domaine-' . $domaine->id,
'title' => '🌐 ' . $domaine->nom,
'start' => $domaine->date_echeance->toDateString(),
'url' => route('domaines.index'),
'backgroundColor' => '#9b59b6',
'extendedProps' => [
'type' => 'Domaine',
]
];
}
// 4. Asset warranties
$assets = \App\Models\Asset::whereNotNull('date_fin_garantie')->get();
foreach ($assets as $asset) {
$events[] = [
'id' => 'asset-' . $asset->id,
'title' => '🛠️ Garantie : ' . $asset->nom . ' (' . $asset->type . ')',
'start' => $asset->date_fin_garantie->toDateString(),
'url' => route('assets.show', $asset->id),
'backgroundColor' => $asset->garantie_expiree ? '#e74c3c' : '#f1c40f',
'extendedProps' => [
'type' => 'Asset (Garantie)',
'statut' => $asset->statut,
]
];
}
return response()->json($events);
}
private function getContratColor($contrat): string
{
if ($contrat->statut === 'expire') return '#e74c3c';
if ($contrat->est_proche_echeance) return '#f39c12';
return '#27ae60';
}
}

View File

@@ -20,6 +20,7 @@ class CommandeController extends Controller
$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->commune_id, fn ($q) => $q->where('commune_id', $request->commune_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))
@@ -38,6 +39,7 @@ class CommandeController extends Controller
'commandes' => $commandes,
'services' => Service::all(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => \App\Models\Commune::orderBy('nom')->get(),
'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut', 'priorite', 'date_from', 'date_to']),
]);
}
@@ -49,6 +51,7 @@ class CommandeController extends Controller
return Inertia::render('Commandes/Create', [
'services' => Service::all(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => \App\Models\Commune::orderBy('nom')->get(),
'categories' => Categorie::active()->orderBy('ordre')->get(),
'articles' => Article::active()->with('categorie')->orderBy('designation')->get(),
]);
@@ -61,6 +64,7 @@ class CommandeController extends Controller
$validated = $request->validate([
'service_id' => 'required|exists:services,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'commune_id' => 'nullable|exists:communes,id',
'objet' => 'required|string|max:255',
'description' => 'nullable|string',
'justification' => 'nullable|string',
@@ -106,10 +110,11 @@ class CommandeController extends Controller
$this->authorize('view', $commande);
$commande->load([
'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur',
'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur', 'commune',
'lignes.categorie',
'historique.user',
'piecesJointes.user',
'assets',
]);
$transitionsDisponibles = collect(Commande::STATUT_TRANSITIONS[$commande->statut] ?? [])
@@ -132,6 +137,7 @@ class CommandeController extends Controller
'commande' => $commande,
'services' => Service::all(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => \App\Models\Commune::orderBy('nom')->get(),
'categories' => Categorie::active()->orderBy('ordre')->get(),
'articles' => Article::active()->with('categorie')->orderBy('designation')->get(),
]);
@@ -144,6 +150,7 @@ class CommandeController extends Controller
$validated = $request->validate([
'service_id' => 'required|exists:services,id',
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
'commune_id' => 'nullable|exists:communes,id',
'objet' => 'required|string|max:255',
'description' => 'nullable|string',
'justification' => 'nullable|string',
@@ -234,7 +241,7 @@ class CommandeController extends Controller
$this->authorize('view', $commande);
$commande->load([
'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur',
'service', 'fournisseur', 'demandeur', 'validateur', 'acheteur', 'commune',
'lignes.categorie',
]);

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use App\Models\Commune;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class CommuneController extends Controller
{
public function index(): Response
{
return Inertia::render('Communes/Index', [
'communes' => Commune::withCount('commandes', 'contrats')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'nom' => 'required|string|max:100|unique:communes,nom',
'code_postal' => 'nullable|string|max:10',
]);
Commune::create($validated);
return back()->with('success', 'Commune créée.');
}
public function update(Request $request, Commune $commune): RedirectResponse
{
$validated = $request->validate([
'nom' => 'required|string|max:100|unique:communes,nom,' . $commune->id,
'code_postal' => 'nullable|string|max:10',
]);
$commune->update($validated);
return back()->with('success', 'Commune mise à jour.');
}
public function destroy(Commune $commune): RedirectResponse
{
if ($commune->commandes()->exists() || $commune->contrats()->exists()) {
return back()->with('error', 'Impossible de supprimer une commune liée à des commandes ou des contrats.');
}
$commune->delete();
return back()->with('success', 'Commune supprimée.');
}
}

View File

@@ -34,6 +34,8 @@ class ContratController extends Controller
}
})->when($request->fournisseur_id, function ($q, $fournisseurId) {
$q->where('fournisseur_id', $fournisseurId);
})->when($request->commune_id, function ($q, $communeId) {
$q->where('commune_id', $communeId);
})->when($request->statut, function ($q, $statut) {
$q->where('statut', $statut);
});
@@ -50,6 +52,7 @@ class ContratController extends Controller
'contrats' => $contrats,
'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => \App\Models\Commune::orderBy('nom')->get(),
'filters' => $request->only(['search', 'service_id', 'fournisseur_id', 'statut']),
'statuts' => Contrat::STATUTS_LABELS,
]);
@@ -62,6 +65,7 @@ class ContratController extends Controller
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(),
'communes' => \App\Models\Commune::orderBy('nom')->get(),
'statuts' => Contrat::STATUTS_LABELS,
]);
}
@@ -102,7 +106,7 @@ class ContratController extends Controller
{
$this->authorize('view', $contrat);
$contrat->load(['fournisseur', 'service', 'piecesJointes.user']);
$contrat->load(['fournisseur', 'service', 'commune', 'piecesJointes.user']);
$contrat->append(['est_proche_echeance', 'est_en_retard']);
return Inertia::render('Contrats/Show', [
@@ -118,6 +122,7 @@ class ContratController extends Controller
'contrat' => $contrat,
'services' => $request->user()->hasRole('admin') ? Service::all() : Service::where('id', $request->user()->service_id)->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => \App\Models\Commune::orderBy('nom')->get(),
'statuts' => Contrat::STATUTS_LABELS,
]);
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Http\Controllers;
use App\Models\Commune;
use App\Models\Contrat;
use App\Models\Fournisseur;
use App\Models\Licence;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class LicenceController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('viewAny', Licence::class);
$query = Licence::with(['fournisseur', 'contrat', 'commune']);
if (!$request->user()->hasRole('admin')) {
$query->where('commune_id', $request->user()->commune_id);
}
$licences = $query->orderBy('date_expiration', 'asc')->paginate(20)->withQueryString();
return Inertia::render('Licences/Index', [
'licences' => $licences,
'filters' => $request->only(['search', 'commune_id', 'fournisseur_id']),
'communes' => Commune::orderBy('nom')->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
]);
}
public function create(): Response
{
$this->authorize('create', Licence::class);
return Inertia::render('Licences/Form', [
'contrats' => Contrat::orderBy('titre')->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => Commune::orderBy('nom')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', Licence::class);
$validated = $request->validate([
'nom' => 'required|string|max:255',
'contrat_id' => 'nullable|exists:contrats,id',
'fournisseur_id' => 'required|exists:fournisseurs,id',
'commune_id' => 'nullable|exists:communes,id',
'cle_licence' => 'nullable|string',
'nombre_sieges_total' => 'required|integer|min:1',
'nombre_sieges_utilises' => 'required|integer|min:0',
'date_acquisition' => 'nullable|date',
'date_expiration' => 'nullable|date',
'type_licence' => 'required|in:perpétuelle,abonnement',
'statut' => 'required|in:active,expirée,résiliée',
'notes' => 'nullable|string',
]);
Licence::create($validated);
return redirect()->route('licences.index')
->with('success', 'Licence créée avec succès.');
}
public function edit(Licence $licence): Response
{
$this->authorize('update', $licence);
return Inertia::render('Licences/Form', [
'licence' => $licence,
'contrats' => Contrat::orderBy('titre')->get(),
'fournisseurs' => Fournisseur::active()->orderBy('nom')->get(),
'communes' => Commune::orderBy('nom')->get(),
]);
}
public function update(Request $request, Licence $licence): RedirectResponse
{
$this->authorize('update', $licence);
$validated = $request->validate([
'nom' => 'required|string|max:255',
'contrat_id' => 'nullable|exists:contrats,id',
'fournisseur_id' => 'required|exists:fournisseurs,id',
'commune_id' => 'nullable|exists:communes,id',
'cle_licence' => 'nullable|string',
'nombre_sieges_total' => 'required|integer|min:1',
'nombre_sieges_utilises' => 'required|integer|min:0',
'date_acquisition' => 'nullable|date',
'date_expiration' => 'nullable|date',
'type_licence' => 'required|in:perpétuelle,abonnement',
'statut' => 'required|in:active,expirée,résiliée',
'notes' => 'nullable|string',
]);
$licence->update($validated);
return redirect()->route('licences.index')
->with('success', 'Licence mise à jour.');
}
public function destroy(Licence $licence): RedirectResponse
{
$this->authorize('delete', $licence);
$licence->delete();
return redirect()->route('licences.index')
->with('success', 'Licence supprimée.');
}
}