feat: Implement full CRUD API for quotes with dedicated model, repository, requests, resource, and database migration.
This commit is contained in:
parent
5d93f9d39a
commit
19b592720e
158
thanasoft-back/app/Http/Controllers/Api/QuoteController.php
Normal file
158
thanasoft-back/app/Http/Controllers/Api/QuoteController.php
Normal file
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreQuoteRequest;
|
||||
use App\Http\Requests\UpdateQuoteRequest;
|
||||
use App\Http\Resources\QuoteResource;
|
||||
use App\Models\Quote;
|
||||
use App\Repositories\QuoteRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class QuoteController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected QuoteRepositoryInterface $quoteRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of quotes.
|
||||
*/
|
||||
public function index(): AnonymousResourceCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$quotes = $this->quoteRepository->all();
|
||||
return QuoteResource::collection($quotes);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching quotes: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des devis.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created quote.
|
||||
*/
|
||||
public function store(StoreQuoteRequest $request): QuoteResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$quote = $this->quoteRepository->create($request->validated());
|
||||
return new QuoteResource($quote);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating quote: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la création du devis.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified quote.
|
||||
*/
|
||||
public function show(string $id): QuoteResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$quote = $this->quoteRepository->find($id);
|
||||
|
||||
if (! $quote) {
|
||||
return response()->json([
|
||||
'message' => 'Devis non trouvé.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return new QuoteResource($quote);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching quote: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'quote_id' => $id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération du devis.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified quote.
|
||||
*/
|
||||
public function update(UpdateQuoteRequest $request, string $id): QuoteResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$updated = $this->quoteRepository->update($id, $request->validated());
|
||||
|
||||
if (! $updated) {
|
||||
return response()->json([
|
||||
'message' => 'Devis non trouvé ou échec de la mise à jour.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$quote = $this->quoteRepository->find($id);
|
||||
return new QuoteResource($quote);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating quote: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'quote_id' => $id,
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la mise à jour du devis.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified quote.
|
||||
*/
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deleted = $this->quoteRepository->delete($id);
|
||||
|
||||
if (! $deleted) {
|
||||
return response()->json([
|
||||
'message' => 'Devis non trouvé ou échec de la suppression.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Devis supprimé avec succès.',
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting quote: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'quote_id' => $id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la suppression du devis.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
thanasoft-back/app/Http/Requests/StoreQuoteRequest.php
Normal file
67
thanasoft-back/app/Http/Requests/StoreQuoteRequest.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreQuoteRequest 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 [
|
||||
'client_id' => 'required|exists:clients,id',
|
||||
'group_id' => 'nullable|exists:client_groups,id',
|
||||
'quote_number' => 'required|string|max:191|unique:quotes,quote_number',
|
||||
'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule',
|
||||
'quote_date' => 'required|date',
|
||||
'valid_until' => 'nullable|date|after_or_equal:quote_date',
|
||||
'currency' => 'required|string|size:3',
|
||||
'total_ht' => 'required|numeric|min:0',
|
||||
'total_tva' => 'required|numeric|min:0',
|
||||
'total_ttc' => 'required|numeric|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'client_id.required' => 'Le client est obligatoire.',
|
||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
||||
'group_id.exists' => 'Le groupe sélectionné est invalide.',
|
||||
'quote_number.required' => 'Le numéro de devis est obligatoire.',
|
||||
'quote_number.string' => 'Le numéro de devis doit être une chaîne de caractères.',
|
||||
'quote_number.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.',
|
||||
'quote_number.unique' => 'Ce numéro de devis existe déjà.',
|
||||
'status.required' => 'Le statut est obligatoire.',
|
||||
'status.in' => 'Le statut sélectionné est invalide.',
|
||||
'quote_date.required' => 'La date du devis est obligatoire.',
|
||||
'quote_date.date' => 'La date du devis n\'est pas valide.',
|
||||
'valid_until.date' => 'La date de validité n\'est pas valide.',
|
||||
'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.',
|
||||
'currency.required' => 'La devise est obligatoire.',
|
||||
'currency.size' => 'La devise doit comporter 3 caractères.',
|
||||
'total_ht.required' => 'Le total HT est obligatoire.',
|
||||
'total_ht.numeric' => 'Le total HT doit être un nombre.',
|
||||
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
|
||||
'total_tva.required' => 'Le total TVA est obligatoire.',
|
||||
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
|
||||
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
|
||||
'total_ttc.required' => 'Le total TTC est obligatoire.',
|
||||
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
|
||||
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
|
||||
];
|
||||
}
|
||||
}
|
||||
59
thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php
Normal file
59
thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateQuoteRequest 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 [
|
||||
'client_id' => 'sometimes|exists:clients,id',
|
||||
'group_id' => 'nullable|exists:client_groups,id',
|
||||
'quote_number' => 'sometimes|string|max:191|unique:quotes,quote_number,' . $this->quote->id,
|
||||
'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule',
|
||||
'quote_date' => 'sometimes|date',
|
||||
'valid_until' => 'nullable|date|after_or_equal:quote_date',
|
||||
'currency' => 'sometimes|string|size:3',
|
||||
'total_ht' => 'sometimes|numeric|min:0',
|
||||
'total_tva' => 'sometimes|numeric|min:0',
|
||||
'total_ttc' => 'sometimes|numeric|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'client_id.exists' => 'Le client sélectionné est invalide.',
|
||||
'group_id.exists' => 'Le groupe sélectionné est invalide.',
|
||||
'quote_number.string' => 'Le numéro de devis doit être une chaîne de caractères.',
|
||||
'quote_number.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.',
|
||||
'quote_number.unique' => 'Ce numéro de devis existe déjà.',
|
||||
'status.in' => 'Le statut sélectionné est invalide.',
|
||||
'quote_date.date' => 'La date du devis n\'est pas valide.',
|
||||
'valid_until.date' => 'La date de validité n\'est pas valide.',
|
||||
'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.',
|
||||
'currency.size' => 'La devise doit comporter 3 caractères.',
|
||||
'total_ht.numeric' => 'Le total HT doit être un nombre.',
|
||||
'total_ht.min' => 'Le total HT ne peut pas être négatif.',
|
||||
'total_tva.numeric' => 'Le total TVA doit être un nombre.',
|
||||
'total_tva.min' => 'Le total TVA ne peut pas être négatif.',
|
||||
'total_ttc.numeric' => 'Le total TTC doit être un nombre.',
|
||||
'total_ttc.min' => 'Le total TTC ne peut pas être négatif.',
|
||||
];
|
||||
}
|
||||
}
|
||||
35
thanasoft-back/app/Http/Resources/QuoteResource.php
Normal file
35
thanasoft-back/app/Http/Resources/QuoteResource.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class QuoteResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'client_id' => $this->client_id,
|
||||
'group_id' => $this->group_id,
|
||||
'quote_number' => $this->quote_number,
|
||||
'status' => $this->status,
|
||||
'quote_date' => $this->quote_date,
|
||||
'valid_until' => $this->valid_until,
|
||||
'currency' => $this->currency,
|
||||
'total_ht' => $this->total_ht,
|
||||
'total_tva' => $this->total_tva,
|
||||
'total_ttc' => $this->total_ttc,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'client' => $this->whenLoaded('client'),
|
||||
'group' => $this->whenLoaded('group'),
|
||||
];
|
||||
}
|
||||
}
|
||||
42
thanasoft-back/app/Models/Quote.php
Normal file
42
thanasoft-back/app/Models/Quote.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Quote extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'client_id',
|
||||
'group_id',
|
||||
'quote_number',
|
||||
'status',
|
||||
'quote_date',
|
||||
'valid_until',
|
||||
'currency',
|
||||
'total_ht',
|
||||
'total_tva',
|
||||
'total_ttc',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quote_date' => 'date',
|
||||
'valid_until' => 'date',
|
||||
'total_ht' => 'decimal:2',
|
||||
'total_tva' => 'decimal:2',
|
||||
'total_ttc' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(ClientGroup::class);
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,7 @@ class RepositoryServiceProvider extends ServiceProvider
|
||||
$this->app->bind(DeceasedDocumentRepositoryInterface::class, DeceasedDocumentRepository::class);
|
||||
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
||||
$this->app->bind(FileRepositoryInterface::class, FileRepository::class);
|
||||
$this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, \App\Repositories\QuoteRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
15
thanasoft-back/app/Repositories/QuoteRepository.php
Normal file
15
thanasoft-back/app/Repositories/QuoteRepository.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Quote;
|
||||
|
||||
class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface
|
||||
{
|
||||
public function __construct(Quote $model)
|
||||
{
|
||||
parent::__construct($model);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
interface QuoteRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
}
|
||||
@ -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('quotes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('client_id');
|
||||
$table->unsignedBigInteger('group_id')->nullable();
|
||||
$table->string('quote_number', 191);
|
||||
$table->enum('status', ['brouillon', 'envoye', 'accepte', 'refuse', 'expire', 'annule'])->default('brouillon')->index('idx_quotes_status');
|
||||
$table->date('quote_date')->default(now());
|
||||
$table->date('valid_until')->nullable();
|
||||
$table->char('currency', 3)->default('EUR');
|
||||
$table->decimal('total_ht', 14, 2)->default(0);
|
||||
$table->decimal('total_tva', 14, 2)->default(0);
|
||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('client_id', 'fk_quotes_client')->references('id')->on('clients');
|
||||
$table->foreign('group_id', 'fk_quotes_group')->references('id')->on('client_groups')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('quotes');
|
||||
}
|
||||
};
|
||||
@ -18,6 +18,7 @@ use App\Http\Controllers\Api\DeceasedDocumentController;
|
||||
use App\Http\Controllers\Api\InterventionController;
|
||||
use App\Http\Controllers\Api\FileController;
|
||||
use App\Http\Controllers\Api\FileAttachmentController;
|
||||
use App\Http\Controllers\Api\QuoteController;
|
||||
|
||||
|
||||
/*
|
||||
@ -61,6 +62,9 @@ Route::middleware('auth:sanctum')->group(function () {
|
||||
|
||||
Route::apiResource('client-categories', ClientCategoryController::class);
|
||||
|
||||
// Quote management
|
||||
Route::apiResource('quotes', QuoteController::class);
|
||||
|
||||
// Fournisseur management
|
||||
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
||||
Route::apiResource('fournisseurs', FournisseurController::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user