Ajout et liste fournisseur

This commit is contained in:
Nyavokevin 2025-10-28 18:03:44 +03:00
parent e924c4f819
commit ca09f6da2f
11 changed files with 584 additions and 0 deletions

View File

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreFournisseurRequest;
use App\Http\Requests\UpdateFournisseurRequest;
use App\Http\Resources\Fournisseur\FournisseurResource;
use App\Http\Resources\Fournisseur\FournisseurCollection;
use App\Repositories\FournisseurRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class FournisseurController extends Controller
{
public function __construct(
private readonly FournisseurRepositoryInterface $fournisseurRepository
) {
}
/**
* Display a listing of fournisseurs.
*/
public function index(Request $request): FournisseurCollection|JsonResponse
{
try {
$perPage = $request->get('per_page', 15);
$filters = [
'search' => $request->get('search'),
'is_active' => $request->get('is_active'),
'sort_by' => $request->get('sort_by', 'created_at'),
'sort_direction' => $request->get('sort_direction', 'desc'),
];
// Remove null filters
$filters = array_filter($filters, function ($value) {
return $value !== null && $value !== '';
});
$fournisseurs = $this->fournisseurRepository->paginate($perPage, $filters);
return new FournisseurCollection($fournisseurs);
} catch (\Exception $e) {
Log::error('Error fetching fournisseurs: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération des fournisseurs.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Store a newly created fournisseur.
*/
public function store(StoreFournisseurRequest $request): FournisseurResource|JsonResponse
{
try {
$fournisseur = $this->fournisseurRepository->create($request->validated());
return new FournisseurResource($fournisseur);
} catch (\Exception $e) {
Log::error('Error creating fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la création du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Display the specified fournisseur.
*/
public function show(string $id): FournisseurResource|JsonResponse
{
try {
$fournisseur = $this->fournisseurRepository->find($id);
if (!$fournisseur) {
return response()->json([
'message' => 'Fournisseur non trouvé.',
], 404);
}
return new FournisseurResource($fournisseur);
} catch (\Exception $e) {
Log::error('Error fetching fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la récupération du fournisseur.',
'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);
}
$fournisseurs = $this->fournisseurRepository->searchByName($name);
return response()->json([
'data' => $fournisseurs,
'count' => $fournisseurs->count(),
'message' => $fournisseurs->count() > 0
? 'Fournisseurs trouvés avec succès.'
: 'Aucun fournisseur trouvé.',
], 200);
} catch (\Exception $e) {
Log::error('Error searching fournisseurs by name: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'search_term' => $name,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la recherche des fournisseurs.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Update the specified fournisseur.
*/
public function update(UpdateFournisseurRequest $request, string $id): FournisseurResource|JsonResponse
{
try {
$updated = $this->fournisseurRepository->update($id, $request->validated());
if (!$updated) {
return response()->json([
'message' => 'Fournisseur non trouvé ou échec de la mise à jour.',
], 404);
}
$fournisseur = $this->fournisseurRepository->find($id);
return new FournisseurResource($fournisseur);
} catch (\Exception $e) {
Log::error('Error updating fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $id,
'data' => $request->validated(),
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la mise à jour du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* Remove the specified fournisseur.
*/
public function destroy(string $id): JsonResponse
{
try {
$deleted = $this->fournisseurRepository->delete($id);
if (!$deleted) {
return response()->json([
'message' => 'Fournisseur non trouvé ou échec de la suppression.',
], 404);
}
return response()->json([
'message' => 'Fournisseur supprimé avec succès.',
], 200);
} catch (\Exception $e) {
Log::error('Error deleting fournisseur: ' . $e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
'fournisseur_id' => $id,
]);
return response()->json([
'message' => 'Une erreur est survenue lors de la suppression du fournisseur.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreFournisseurRequest 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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'notes' => 'nullable|string',
'is_active' => 'boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du fournisseur est obligatoire.',
'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.',
'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateFournisseurRequest 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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'sometimes|required|string|max:255',
'vat_number' => 'nullable|string|max:32',
'siret' => 'nullable|string|max:20',
'email' => 'nullable|email|max:191',
'phone' => 'nullable|string|max:50',
'billing_address_line1' => 'nullable|string|max:255',
'billing_address_line2' => 'nullable|string|max:255',
'billing_postal_code' => 'nullable|string|max:20',
'billing_city' => 'nullable|string|max:191',
'billing_country_code' => 'nullable|string|size:2',
'notes' => 'nullable|string',
'is_active' => 'boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'Le nom du fournisseur est obligatoire.',
'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.',
'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.',
'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.',
'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.',
'email.email' => 'L\'adresse email doit être valide.',
'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.',
'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.',
'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.',
'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.',
'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.',
'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.',
'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.',
'is_active.boolean' => 'Le statut actif doit être vrai ou faux.',
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Resources\Fournisseur;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class FournisseurCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
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' => [
'active' => $this->collection->where('is_active', true)->count(),
'inactive' => $this->collection->where('is_active', false)->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' => 'Fournisseurs récupérés avec succès',
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Resources\Fournisseur;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FournisseurResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'commercial' => $this->commercial(),
'name' => $this->name,
'vat_number' => $this->vat_number,
'siret' => $this->siret,
'email' => $this->email,
'phone' => $this->phone,
'billing_address' => [
'line1' => $this->billing_address_line1,
'line2' => $this->billing_address_line2,
'postal_code' => $this->billing_postal_code,
'city' => $this->billing_city,
'country_code' => $this->billing_country_code,
'full_address' => $this->billing_address,
],
'notes' => $this->notes,
'is_active' => $this->is_active,
'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,54 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Fournisseur extends Model
{
protected $fillable = [
'name',
'vat_number',
'siret',
'email',
'phone',
'billing_address_line1',
'billing_address_line2',
'billing_postal_code',
'billing_city',
'billing_country_code',
'notes',
'is_active',
'user_id',
];
protected $casts = [
'is_active' => 'boolean',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function commercial(): ?string
{
return $this->user ? $this->user->name : 'Système';
}
/**
* Get the full billing address as a string.
*/
public function getBillingAddressAttribute(): ?string
{
$parts = array_filter([
$this->billing_address_line1,
$this->billing_address_line2,
$this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city,
$this->billing_country_code,
]);
return !empty($parts) ? implode(', ', $parts) : null;
}
}

View File

@ -31,6 +31,10 @@ class AppServiceProvider extends ServiceProvider
$this->app->bind(\App\Repositories\ClientLocationRepositoryInterface::class, function ($app) { $this->app->bind(\App\Repositories\ClientLocationRepositoryInterface::class, function ($app) {
return new \App\Repositories\ClientLocationRepository($app->make(\App\Models\ClientLocation::class)); return new \App\Repositories\ClientLocationRepository($app->make(\App\Models\ClientLocation::class));
}); });
$this->app->bind(\App\Repositories\FournisseurRepositoryInterface::class, function ($app) {
return new \App\Repositories\FournisseurRepository($app->make(\App\Models\Fournisseur::class));
});
} }
/** /**

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Fournisseur;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class FournisseurRepository extends BaseRepository implements FournisseurRepositoryInterface
{
public function __construct(Fournisseur $model)
{
parent::__construct($model);
}
/**
* Get paginated fournisseurs
*/
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
{
$query = $this->model->newQuery();
// Apply filters
if (!empty($filters['search'])) {
$query->where(function ($q) use ($filters) {
$q->where('name', 'like', '%' . $filters['search'] . '%')
->orWhere('email', 'like', '%' . $filters['search'] . '%')
->orWhere('vat_number', 'like', '%' . $filters['search'] . '%')
->orWhere('siret', 'like', '%' . $filters['search'] . '%');
});
}
if (isset($filters['is_active'])) {
$query->where('is_active', $filters['is_active']);
}
// Apply sorting
$sortField = $filters['sort_by'] ?? 'created_at';
$sortDirection = $filters['sort_direction'] ?? 'desc';
$query->orderBy($sortField, $sortDirection);
return $query->paginate($perPage);
}
public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false)
{
$query = $this->model->newQuery();
if ($exactMatch) {
$query->where('name', $name);
} else {
$query->where('name', 'like', '%' . $name . '%');
}
return $query->get();
}
}

View File

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

View File

@ -0,0 +1,40 @@
<?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('fournisseurs', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('vat_number', 32)->nullable();
$table->string('siret', 20)->nullable();
$table->string('email', 191)->nullable();
$table->string('phone', 50)->nullable();
$table->string('billing_address_line1')->nullable();
$table->string('billing_address_line2')->nullable();
$table->string('billing_postal_code', 20)->nullable();
$table->string('billing_city', 191)->nullable();
$table->string('billing_country_code', 2)->nullable();
$table->text('notes')->nullable();
$table->boolean('is_active')->default(true);
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('fournisseurs');
}
};

View File

@ -7,6 +7,7 @@ use App\Http\Controllers\Api\ClientGroupController;
use App\Http\Controllers\Api\ClientLocationController; 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;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -49,4 +50,7 @@ Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('client-categories', ClientCategoryController::class); Route::apiResource('client-categories', ClientCategoryController::class);
// Fournisseur management
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
Route::apiResource('fournisseurs', FournisseurController::class);
}); });