Feat: API sous-traitant

This commit is contained in:
kevin 2026-05-22 10:01:35 +03:00
parent 9a52bddd1a
commit 7506bdcf8b
11 changed files with 544 additions and 0 deletions

View File

@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreSousTraitantRequest;
use App\Http\Requests\UpdateSousTraitantRequest;
use App\Http\Resources\SousTraitant\SousTraitantCollection;
use App\Http\Resources\SousTraitant\SousTraitantResource;
use App\Repositories\SousTraitantRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class SousTraitantController extends Controller
{
public function __construct(
private readonly SousTraitantRepositoryInterface $sousTraitantRepository
) {
}
public function index(Request $request): SousTraitantCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'statut' => $request->get('statut'),
'sort_by' => $request->get('sort_by', 'created_at'),
'sort_direction' => $request->get('sort_direction', 'desc'),
];
$filters = array_filter($filters, function ($value) {
return $value !== null && $value !== '';
});
$sousTraitants = $this->sousTraitantRepository->paginate($perPage, $filters);
return new SousTraitantCollection($sousTraitants);
} catch (\Exception $e) {
Log::error('Error fetching sous-traitants: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des sous-traitants.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function store(StoreSousTraitantRequest $request): SousTraitantResource|JsonResponse
{
try {
$sousTraitant = $this->sousTraitantRepository->create($request->validated());
return new SousTraitantResource($sousTraitant);
} catch (\Exception $e) {
Log::error('Error creating sous-traitant: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du sous-traitant.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function show(string $id): SousTraitantResource|JsonResponse
{
try {
$sousTraitant = $this->sousTraitantRepository->find($id);
if (!$sousTraitant) {
return response()->json([
'message' => 'Sous-traitant non trouvé.',
], 404);
}
return new SousTraitantResource($sousTraitant);
} catch (\Exception $e) {
Log::error('Error fetching sous-traitant: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'sous_traitant_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du sous-traitant.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function searchBy(Request $request): JsonResponse
{
try {
$name = $request->get('name', '');
if (empty($name)) {
return response()->json([
'message' => 'Le paramètre "name" est requis.',
], 400);
}
$sousTraitants = $this->sousTraitantRepository->searchByName($name);
return response()->json([
'data' => $sousTraitants,
'count' => $sousTraitants->count(),
'message' => $sousTraitants->count() > 0
? 'Sous-traitants trouvés avec succès.'
: 'Aucun sous-traitant trouvé.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching sous-traitants by name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des sous-traitants.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function update(UpdateSousTraitantRequest $request, string $id): SousTraitantResource|JsonResponse
{
try {
$updated = $this->sousTraitantRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Sous-traitant non trouvé ou échec de la mise à jour.',
], 404);
}
$sousTraitant = $this->sousTraitantRepository->find($id);
return new SousTraitantResource($sousTraitant);
} catch (\Exception $e) {
Log::error('Error updating sous-traitant: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'sous_traitant_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du sous-traitant.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->sousTraitantRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Sous-traitant non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Sous-traitant supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting sous-traitant: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'sous_traitant_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du sous-traitant.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreSousTraitantRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'nom_entreprise' => 'required|string|max:255',
'siret' => 'nullable|string|max:20',
'forme_juridique' => 'nullable|string|max:100',
'code_ape' => 'nullable|string|max:20',
'adresse' => 'nullable|string',
'contact_principal' => 'required|string|max:255',
'telephone' => 'nullable|string|max:50',
'email' => 'nullable|email|max:191',
'site_web' => 'nullable|url|max:191',
'numero_contrat' => 'nullable|string|max:100',
'montant_contrat' => 'nullable|numeric|min:0',
'date_debut_contrat' => 'nullable|date',
'date_fin_contrat' => 'nullable|date|after_or_equal:date_debut_contrat',
'type_prestation' => 'nullable|string|max:255',
'conditions_paiement' => 'nullable|string',
'statut' => ['nullable', Rule::in(['actif', 'inactif', 'en_evaluation'])],
'note_qualite' => 'nullable|numeric|min:0|max:5',
'certifications_labels' => 'nullable|array',
'certifications_labels.*' => 'string|max:255',
];
}
public function messages(): array
{
return [
'nom_entreprise.required' => 'Le nom de l\'entreprise est obligatoire.',
'contact_principal.required' => 'Le contact principal est obligatoire.',
'email.email' => 'L\'adresse email doit être valide.',
'site_web.url' => 'Le site web doit être une URL valide.',
'montant_contrat.numeric' => 'Le montant du contrat doit être numérique.',
'montant_contrat.min' => 'Le montant du contrat doit être supérieur ou égal à 0.',
'date_fin_contrat.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.',
'statut.in' => 'Le statut sélectionné est invalide.',
'note_qualite.numeric' => 'La note de qualité doit être numérique.',
'note_qualite.min' => 'La note de qualité doit être au minimum de 0.',
'note_qualite.max' => 'La note de qualité doit être au maximum de 5.',
'certifications_labels.array' => 'Les certifications doivent être envoyées sous forme de liste.',
'certifications_labels.*.string' => 'Chaque certification doit être une chaîne de caractères.',
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateSousTraitantRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'nom_entreprise' => 'sometimes|required|string|max:255',
'siret' => 'nullable|string|max:20',
'forme_juridique' => 'nullable|string|max:100',
'code_ape' => 'nullable|string|max:20',
'adresse' => 'nullable|string',
'contact_principal' => 'sometimes|required|string|max:255',
'telephone' => 'nullable|string|max:50',
'email' => 'nullable|email|max:191',
'site_web' => 'nullable|url|max:191',
'numero_contrat' => 'nullable|string|max:100',
'montant_contrat' => 'nullable|numeric|min:0',
'date_debut_contrat' => 'nullable|date',
'date_fin_contrat' => 'nullable|date|after_or_equal:date_debut_contrat',
'type_prestation' => 'nullable|string|max:255',
'conditions_paiement' => 'nullable|string',
'statut' => ['nullable', Rule::in(['actif', 'inactif', 'en_evaluation'])],
'note_qualite' => 'nullable|numeric|min:0|max:5',
'certifications_labels' => 'nullable|array',
'certifications_labels.*' => 'string|max:255',
];
}
public function messages(): array
{
return [
'nom_entreprise.required' => 'Le nom de l\'entreprise est obligatoire.',
'contact_principal.required' => 'Le contact principal est obligatoire.',
'email.email' => 'L\'adresse email doit être valide.',
'site_web.url' => 'Le site web doit être une URL valide.',
'montant_contrat.numeric' => 'Le montant du contrat doit être numérique.',
'montant_contrat.min' => 'Le montant du contrat doit être supérieur ou égal à 0.',
'date_fin_contrat.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.',
'statut.in' => 'Le statut sélectionné est invalide.',
'note_qualite.numeric' => 'La note de qualité doit être numérique.',
'note_qualite.min' => 'La note de qualité doit être au minimum de 0.',
'note_qualite.max' => 'La note de qualité doit être au maximum de 5.',
'certifications_labels.array' => 'Les certifications doivent être envoyées sous forme de liste.',
'certifications_labels.*.string' => 'Chaque certification doit être une chaîne de caractères.',
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Resources\SousTraitant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class SousTraitantCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
'stats' => [
'actif' => $this->collection->where('statut', 'actif')->count(),
'inactif' => $this->collection->where('statut', 'inactif')->count(),
'en_evaluation' => $this->collection->where('statut', 'en_evaluation')->count(),
],
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
public function with(Request $request): array
{
return [
'status' => 'success',
'message' => 'Sous-traitants récupérés avec succès',
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Resources\SousTraitant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class SousTraitantResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'nom_entreprise' => $this->nom_entreprise,
'siret' => $this->siret,
'forme_juridique' => $this->forme_juridique,
'code_ape' => $this->code_ape,
'adresse' => $this->adresse,
'contact_principal' => $this->contact_principal,
'telephone' => $this->telephone,
'email' => $this->email,
'site_web' => $this->site_web,
'numero_contrat' => $this->numero_contrat,
'montant_contrat' => $this->montant_contrat,
'date_debut_contrat' => $this->date_debut_contrat?->format('Y-m-d'),
'date_fin_contrat' => $this->date_fin_contrat?->format('Y-m-d'),
'type_prestation' => $this->type_prestation,
'conditions_paiement' => $this->conditions_paiement,
'statut' => $this->statut,
'note_qualite' => $this->note_qualite,
'certifications_labels' => $this->certifications_labels ?? [],
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SousTraitant extends Model
{
use HasFactory;
protected $table = 'sous_traitants';
protected $fillable = [
'nom_entreprise',
'siret',
'forme_juridique',
'code_ape',
'adresse',
'contact_principal',
'telephone',
'email',
'site_web',
'numero_contrat',
'montant_contrat',
'date_debut_contrat',
'date_fin_contrat',
'type_prestation',
'conditions_paiement',
'statut',
'note_qualite',
'certifications_labels',
];
protected $casts = [
'montant_contrat' => 'decimal:2',
'date_debut_contrat' => 'date',
'date_fin_contrat' => 'date',
'note_qualite' => 'decimal:1',
'certifications_labels' => 'array',
];
}

View File

@ -48,6 +48,10 @@ class AppServiceProvider extends ServiceProvider
return new \App\Repositories\FournisseurRepository($app->make(\App\Models\Fournisseur::class)); return new \App\Repositories\FournisseurRepository($app->make(\App\Models\Fournisseur::class));
}); });
$this->app->bind(\App\Repositories\SousTraitantRepositoryInterface::class, function ($app) {
return new \App\Repositories\SousTraitantRepository($app->make(\App\Models\SousTraitant::class));
});
$this->app->bind(\App\Repositories\ProductRepositoryInterface::class, function ($app) { $this->app->bind(\App\Repositories\ProductRepositoryInterface::class, function ($app) {
return new \App\Repositories\ProductRepository($app->make(\App\Models\Product::class)); return new \App\Repositories\ProductRepository($app->make(\App\Models\Product::class));
}); });

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\SousTraitant;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class SousTraitantRepository extends BaseRepository implements SousTraitantRepositoryInterface
{
public function __construct(SousTraitant $model)
{
parent::__construct($model);
}
/**
* Get paginated sous-traitants.
*/
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
{
$query = $this->model->newQuery();
if (!empty($filters['search'])) {
$query->where(function ($q) use ($filters) {
$q->where('nom_entreprise', 'like', '%' . $filters['search'] . '%')
->orWhere('contact_principal', 'like', '%' . $filters['search'] . '%')
->orWhere('email', 'like', '%' . $filters['search'] . '%')
->orWhere('siret', 'like', '%' . $filters['search'] . '%')
->orWhere('numero_contrat', 'like', '%' . $filters['search'] . '%')
->orWhere('type_prestation', 'like', '%' . $filters['search'] . '%');
});
}
if (!empty($filters['statut'])) {
$query->where('statut', $filters['statut']);
}
$sortField = $filters['sort_by'] ?? 'created_at';
$sortDirection = $filters['sort_direction'] ?? 'desc';
$query->orderBy($sortField, $sortDirection);
return $query->paginate($perPage);
}
public function searchByName(string $name)
{
return $this->model
->newQuery()
->where('nom_entreprise', 'like', '%' . $name . '%')
->get();
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
interface SousTraitantRepositoryInterface extends BaseRepositoryInterface
{
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator;
}

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sous_traitants', function (Blueprint $table) {
$table->id();
$table->string('nom_entreprise');
$table->string('siret', 20)->nullable();
$table->string('forme_juridique', 100)->nullable();
$table->string('code_ape', 20)->nullable();
$table->text('adresse')->nullable();
$table->string('contact_principal');
$table->string('telephone', 50)->nullable();
$table->string('email', 191)->nullable();
$table->string('site_web', 191)->nullable();
$table->string('numero_contrat', 100)->nullable();
$table->decimal('montant_contrat', 12, 2)->nullable();
$table->date('date_debut_contrat')->nullable();
$table->date('date_fin_contrat')->nullable();
$table->string('type_prestation')->nullable();
$table->text('conditions_paiement')->nullable();
$table->enum('statut', ['actif', 'inactif', 'en_evaluation'])->default('actif');
$table->decimal('note_qualite', 3, 1)->nullable();
$table->json('certifications_labels')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sous_traitants');
}
};

View File

@ -8,6 +8,7 @@ use App\Http\Controllers\Api\ClientLocationController;
use App\Http\Controllers\Api\ContactController; use App\Http\Controllers\Api\ContactController;
use App\Http\Controllers\Api\ClientCategoryController; use App\Http\Controllers\Api\ClientCategoryController;
use App\Http\Controllers\Api\FournisseurController; use App\Http\Controllers\Api\FournisseurController;
use App\Http\Controllers\Api\SousTraitantController;
use App\Http\Controllers\Api\ProductController; use App\Http\Controllers\Api\ProductController;
use App\Http\Controllers\Api\ProductCategoryController; use App\Http\Controllers\Api\ProductCategoryController;
use App\Http\Controllers\Api\EmployeeController; use App\Http\Controllers\Api\EmployeeController;
@ -131,6 +132,8 @@ Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('purchase-orders', PurchaseOrderController::class); Route::apiResource('purchase-orders', PurchaseOrderController::class);
Route::apiResource('fournisseurs', FournisseurController::class); Route::apiResource('fournisseurs', FournisseurController::class);
Route::get('fournisseurs/{fournisseurId}/contacts', [ContactController::class, 'getContactsByFournisseur']); Route::get('fournisseurs/{fournisseurId}/contacts', [ContactController::class, 'getContactsByFournisseur']);
Route::get('/sous-traitants/searchBy', [SousTraitantController::class, 'searchBy']);
Route::apiResource('sous-traitants', SousTraitantController::class);
// Product management // Product management
Route::get('/products/searchBy', [ProductController::class, 'searchBy']); Route::get('/products/searchBy', [ProductController::class, 'searchBy']);