add api produit categorie, et page categori produit
This commit is contained in:
parent
638af78e1f
commit
aa306f5d19
@ -0,0 +1,349 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreProductCategoryRequest;
|
||||||
|
use App\Http\Requests\UpdateProductCategoryRequest;
|
||||||
|
use App\Http\Resources\ProductCategory\ProductCategoryResource;
|
||||||
|
use App\Http\Resources\ProductCategory\ProductCategoryCollection;
|
||||||
|
use App\Repositories\ProductCategoryRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ProductCategoryController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProductCategoryRepositoryInterface $productCategoryRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of product categories.
|
||||||
|
*/
|
||||||
|
public function index(Request $request): ProductCategoryCollection|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$perPage = $request->get('per_page', 15);
|
||||||
|
$filters = [
|
||||||
|
'search' => $request->get('search'),
|
||||||
|
'active' => $request->get('active'),
|
||||||
|
'parent_id' => $request->get('parent_id'),
|
||||||
|
'sort_by' => $request->get('sort_by', 'name'),
|
||||||
|
'sort_direction' => $request->get('sort_direction', 'asc'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Remove null filters
|
||||||
|
$filters = array_filter($filters, function ($value) {
|
||||||
|
return $value !== null && $value !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
$categories = $this->productCategoryRepository->paginate($perPage, $filters);
|
||||||
|
|
||||||
|
return new ProductCategoryCollection($categories);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching product categories: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des catégories de produits.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created product category.
|
||||||
|
*/
|
||||||
|
public function store(StoreProductCategoryRequest $request): ProductCategoryResource|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$category = $this->productCategoryRepository->create($request->validated());
|
||||||
|
return new ProductCategoryResource($category);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating product category: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'data' => $request->validated(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la création de la catégorie.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified product category.
|
||||||
|
*/
|
||||||
|
public function show(string $id): ProductCategoryResource|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$category = $this->productCategoryRepository->find($id);
|
||||||
|
|
||||||
|
if (!$category) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Catégorie non trouvée.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProductCategoryResource($category);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching product category: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'category_id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération de la catégorie.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified product category.
|
||||||
|
*/
|
||||||
|
public function update(UpdateProductCategoryRequest $request, string $id): ProductCategoryResource|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$updated = $this->productCategoryRepository->update($id, $request->validated());
|
||||||
|
|
||||||
|
if (!$updated) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Catégorie non trouvée ou échec de la mise à jour.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $this->productCategoryRepository->find($id);
|
||||||
|
return new ProductCategoryResource($category);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating product category: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'category_id' => $id,
|
||||||
|
'data' => $request->validated(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la mise à jour de la catégorie.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified product category.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Check if category can be deleted
|
||||||
|
if (!$this->productCategoryRepository->canDelete($id)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Impossible de supprimer cette catégorie. Elle peut avoir des sous-catégories ou des produits associés.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = $this->productCategoryRepository->delete($id);
|
||||||
|
|
||||||
|
if (!$deleted) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Catégorie non trouvée ou échec de la suppression.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Catégorie supprimée avec succès.',
|
||||||
|
], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting product category: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'category_id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la suppression de la catégorie.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active categories only
|
||||||
|
*/
|
||||||
|
public function active(): ProductCategoryCollection|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$categories = $this->productCategoryRepository->getActive();
|
||||||
|
return new ProductCategoryCollection(collect(['data' => $categories]));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching active product categories: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des catégories actives.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get root categories (no parent)
|
||||||
|
*/
|
||||||
|
public function roots(): ProductCategoryCollection|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$categories = $this->productCategoryRepository->getRoots();
|
||||||
|
return new ProductCategoryCollection(collect(['data' => $categories]));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching root product categories: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des catégories racine.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories with their children (hierarchical structure)
|
||||||
|
*/
|
||||||
|
public function hierarchical(): ProductCategoryCollection|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$categories = $this->productCategoryRepository->getWithChildren();
|
||||||
|
return new ProductCategoryCollection(collect(['data' => $categories]));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching hierarchical product categories: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération de la structure hiérarchique.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search categories by name, code or description
|
||||||
|
*/
|
||||||
|
public function search(Request $request): ProductCategoryCollection|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$term = $request->get('term', '');
|
||||||
|
$perPage = $request->get('per_page', 15);
|
||||||
|
|
||||||
|
if (empty($term)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Le paramètre "term" est requis pour la recherche.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = $this->productCategoryRepository->search($term, $perPage);
|
||||||
|
|
||||||
|
return new ProductCategoryCollection($categories);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error searching product categories: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'search_term' => $term,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la recherche des catégories.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get category statistics
|
||||||
|
*/
|
||||||
|
public function statistics(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stats = $this->productCategoryRepository->getStatistics();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $stats,
|
||||||
|
'message' => 'Statistiques des catégories récupérées avec succès.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching product category statistics: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des statistiques.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle category active status
|
||||||
|
*/
|
||||||
|
public function toggleActive(Request $request, string $id): ProductCategoryResource|JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->validate([
|
||||||
|
'active' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$category = $this->productCategoryRepository->find($id);
|
||||||
|
|
||||||
|
if (!$category) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Catégorie non trouvée.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = $this->productCategoryRepository->update($id, [
|
||||||
|
'active' => $request->boolean('active')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$updated) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Échec de la mise à jour du statut.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $this->productCategoryRepository->find($id);
|
||||||
|
return new ProductCategoryResource($category);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error toggling product category status: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'category_id' => $id,
|
||||||
|
'data' => $request->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la mise à jour du statut.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,7 +67,22 @@ class ProductController extends Controller
|
|||||||
public function store(StoreProductRequest $request): ProductResource|JsonResponse
|
public function store(StoreProductRequest $request): ProductResource|JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$product = $this->productRepository->create($request->validated());
|
$validatedData = $request->validated();
|
||||||
|
|
||||||
|
// Handle image upload
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
// Create product without image first
|
||||||
|
$product = $this->productRepository->create($validatedData);
|
||||||
|
|
||||||
|
// Upload and attach image
|
||||||
|
$imagePath = $product->uploadImage($request->file('image'));
|
||||||
|
|
||||||
|
// Refresh product to get updated data
|
||||||
|
$product = $this->productRepository->find($product->id);
|
||||||
|
} else {
|
||||||
|
$product = $this->productRepository->create($validatedData);
|
||||||
|
}
|
||||||
|
|
||||||
return new ProductResource($product);
|
return new ProductResource($product);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error creating product: ' . $e->getMessage(), [
|
Log::error('Error creating product: ' . $e->getMessage(), [
|
||||||
@ -240,7 +255,29 @@ class ProductController extends Controller
|
|||||||
public function update(UpdateProductRequest $request, string $id): ProductResource|JsonResponse
|
public function update(UpdateProductRequest $request, string $id): ProductResource|JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$updated = $this->productRepository->update($id, $request->validated());
|
$validatedData = $request->validated();
|
||||||
|
$product = $this->productRepository->find($id);
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Produit non trouvé.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image upload/removal
|
||||||
|
if ($request->boolean('remove_image')) {
|
||||||
|
// Remove existing image
|
||||||
|
$product->deleteImage();
|
||||||
|
} elseif ($request->hasFile('image')) {
|
||||||
|
// Upload new image
|
||||||
|
$product->uploadImage($request->file('image'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove image-related fields from validated data before updating other fields
|
||||||
|
unset($validatedData['image'], $validatedData['remove_image']);
|
||||||
|
|
||||||
|
// Update other product fields
|
||||||
|
$updated = $this->productRepository->update($id, $validatedData);
|
||||||
|
|
||||||
if (!$updated) {
|
if (!$updated) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreProductCategoryRequest 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 [
|
||||||
|
'parent_id' => 'nullable|exists:product_categories,id',
|
||||||
|
'code' => 'required|string|max:64|unique:product_categories,code',
|
||||||
|
'name' => 'required|string|max:191',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'active' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'parent_id.exists' => 'La catégorie parente sélectionnée n\'existe pas.',
|
||||||
|
'code.required' => 'Le code de la catégorie est obligatoire.',
|
||||||
|
'code.string' => 'Le code de la catégorie doit être une chaîne de caractères.',
|
||||||
|
'code.max' => 'Le code de la catégorie ne peut pas dépasser 64 caractères.',
|
||||||
|
'code.unique' => 'Ce code de catégorie existe déjà.',
|
||||||
|
'name.required' => 'Le nom de la catégorie est obligatoire.',
|
||||||
|
'name.string' => 'Le nom de la catégorie doit être une chaîne de caractères.',
|
||||||
|
'name.max' => 'Le nom de la catégorie ne peut pas dépasser 191 caractères.',
|
||||||
|
'description.string' => 'La description doit être une chaîne de caractères.',
|
||||||
|
'active.boolean' => 'Le statut actif doit être un booléen.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,7 +35,8 @@ class StoreProductRequest extends FormRequest
|
|||||||
'conditionnement_nom' => 'nullable|string|max:191',
|
'conditionnement_nom' => 'nullable|string|max:191',
|
||||||
'conditionnement_quantite' => 'nullable|numeric|min:0',
|
'conditionnement_quantite' => 'nullable|numeric|min:0',
|
||||||
'conditionnement_unite' => 'nullable|string|max:50',
|
'conditionnement_unite' => 'nullable|string|max:50',
|
||||||
'photo_url' => 'nullable|url|max:500',
|
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||||
|
'remove_image' => 'nullable|boolean',
|
||||||
'fiche_technique_url' => 'nullable|url|max:500',
|
'fiche_technique_url' => 'nullable|url|max:500',
|
||||||
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
|
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
|
||||||
];
|
];
|
||||||
@ -78,8 +79,9 @@ class StoreProductRequest extends FormRequest
|
|||||||
'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.',
|
'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.',
|
||||||
'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.',
|
'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.',
|
||||||
'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.',
|
'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.',
|
||||||
'photo_url.url' => 'L\'URL de la photo doit être une URL valide.',
|
'image.image' => 'Le fichier doit être une image valide.',
|
||||||
'photo_url.max' => 'L\'URL de la photo ne peut pas dépasser 500 caractères.',
|
'image.mimes' => 'L\'image doit être de type: jpeg, png, jpg, gif ou svg.',
|
||||||
|
'image.max' => 'L\'image ne peut pas dépasser 2MB.',
|
||||||
'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.',
|
'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.',
|
||||||
'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.',
|
'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.',
|
||||||
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
|
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateProductCategoryRequest 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
|
||||||
|
{
|
||||||
|
$categoryId = $this->route('id');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'parent_id' => 'nullable|exists:product_categories,id',
|
||||||
|
'code' => "nullable|string|max:64|unique:product_categories,code,{$categoryId}",
|
||||||
|
'name' => 'nullable|string|max:191',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'active' => 'nullable|boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'parent_id.exists' => 'La catégorie parente sélectionnée n\'existe pas.',
|
||||||
|
'code.string' => 'Le code de la catégorie doit être une chaîne de caractères.',
|
||||||
|
'code.max' => 'Le code de la catégorie ne peut pas dépasser 64 caractères.',
|
||||||
|
'code.unique' => 'Ce code de catégorie existe déjà.',
|
||||||
|
'name.string' => 'Le nom de la catégorie doit être une chaîne de caractères.',
|
||||||
|
'name.max' => 'Le nom de la catégorie ne peut pas dépasser 191 caractères.',
|
||||||
|
'description.string' => 'La description doit être une chaîne de caractères.',
|
||||||
|
'active.boolean' => 'Le statut actif doit être un booléen.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,9 +21,11 @@ class UpdateProductRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$productId = $this->route('id');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'nom' => 'required|string|max:255',
|
'nom' => 'required|string|max:255',
|
||||||
'reference' => 'required|string|max:100|unique:products,reference,' . $this->product?->id,
|
'reference' => "nullable",
|
||||||
'categorie' => 'required|string|max:191',
|
'categorie' => 'required|string|max:191',
|
||||||
'fabricant' => 'nullable|string|max:191',
|
'fabricant' => 'nullable|string|max:191',
|
||||||
'stock_actuel' => 'required|numeric|min:0',
|
'stock_actuel' => 'required|numeric|min:0',
|
||||||
@ -35,7 +37,8 @@ class UpdateProductRequest extends FormRequest
|
|||||||
'conditionnement_nom' => 'nullable|string|max:191',
|
'conditionnement_nom' => 'nullable|string|max:191',
|
||||||
'conditionnement_quantite' => 'nullable|numeric|min:0',
|
'conditionnement_quantite' => 'nullable|numeric|min:0',
|
||||||
'conditionnement_unite' => 'nullable|string|max:50',
|
'conditionnement_unite' => 'nullable|string|max:50',
|
||||||
'photo_url' => 'nullable|url|max:500',
|
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||||
|
'remove_image' => 'nullable|boolean',
|
||||||
'fiche_technique_url' => 'nullable|url|max:500',
|
'fiche_technique_url' => 'nullable|url|max:500',
|
||||||
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
|
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
|
||||||
];
|
];
|
||||||
@ -78,8 +81,9 @@ class UpdateProductRequest extends FormRequest
|
|||||||
'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.',
|
'conditionnement_quantite.min' => 'La quantité du conditionnement doit être supérieure ou égal à 0.',
|
||||||
'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.',
|
'conditionnement_unite.string' => 'L\'unité du conditionnement doit être une chaîne de caractères.',
|
||||||
'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.',
|
'conditionnement_unite.max' => 'L\'unité du conditionnement ne peut pas dépasser 50 caractères.',
|
||||||
'photo_url.url' => 'L\'URL de la photo doit être une URL valide.',
|
'image.image' => 'Le fichier doit être une image valide.',
|
||||||
'photo_url.max' => 'L\'URL de la photo ne peut pas dépasser 500 caractères.',
|
'image.mimes' => 'L\'image doit être de type: jpeg, png, jpg, gif ou svg.',
|
||||||
|
'image.max' => 'L\'image ne peut pas dépasser 2MB.',
|
||||||
'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.',
|
'fiche_technique_url.url' => 'L\'URL de la fiche technique doit être une URL valide.',
|
||||||
'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.',
|
'fiche_technique_url.max' => 'L\'URL de la fiche technique ne peut pas dépasser 500 caractères.',
|
||||||
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
|
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\ProductCategory;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
|
class ProductCategoryCollection extends ResourceCollection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource collection into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
if ($this->resource instanceof \Illuminate\Pagination\LengthAwarePaginator) {
|
||||||
|
return [
|
||||||
|
'data' => ProductCategoryResource::collection($this->collection),
|
||||||
|
'pagination' => [
|
||||||
|
'current_page' => $this->currentPage(),
|
||||||
|
'from' => $this->firstItem(),
|
||||||
|
'last_page' => $this->lastPage(),
|
||||||
|
'per_page' => $this->perPage(),
|
||||||
|
'to' => $this->lastItem(),
|
||||||
|
'total' => $this->total(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => ProductCategoryResource::collection($this->collection),
|
||||||
|
'count' => $this->collection->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\ProductCategory;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class ProductCategoryResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'parent_id' => $this->parent_id,
|
||||||
|
'code' => $this->code,
|
||||||
|
'name' => $this->name,
|
||||||
|
'description' => $this->description,
|
||||||
|
'active' => $this->active,
|
||||||
|
'path' => $this->path,
|
||||||
|
'has_children' => $this->hasChildren(),
|
||||||
|
'has_products' => $this->hasProducts(),
|
||||||
|
'children_count' => $this->children()->count(),
|
||||||
|
'products_count' => $this->products()->count(),
|
||||||
|
|
||||||
|
// Parent information
|
||||||
|
'parent' => $this->whenLoaded('parent', function () {
|
||||||
|
return new ProductCategoryResource($this->parent);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Children information
|
||||||
|
'children' => $this->whenLoaded('children', function () {
|
||||||
|
return ProductCategoryResource::collection($this->children);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class Product extends Model
|
class Product extends Model
|
||||||
{
|
{
|
||||||
@ -21,7 +22,7 @@ class Product extends Model
|
|||||||
'conditionnement_nom',
|
'conditionnement_nom',
|
||||||
'conditionnement_quantite',
|
'conditionnement_quantite',
|
||||||
'conditionnement_unite',
|
'conditionnement_unite',
|
||||||
'photo_url',
|
'image',
|
||||||
'fiche_technique_url',
|
'fiche_technique_url',
|
||||||
'fournisseur_id',
|
'fournisseur_id',
|
||||||
];
|
];
|
||||||
@ -41,4 +42,73 @@ class Product extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Fournisseur::class);
|
return $this->belongsTo(Fournisseur::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the category that owns the product.
|
||||||
|
*/
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductCategory::class, 'categorie', 'name');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle image upload
|
||||||
|
*/
|
||||||
|
public function uploadImage($image)
|
||||||
|
{
|
||||||
|
if ($image) {
|
||||||
|
// Delete old image if exists
|
||||||
|
if ($this->image) {
|
||||||
|
$this->deleteImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new image
|
||||||
|
$imageName = time() . '_' . uniqid() . '.' . $image->getClientOriginalExtension();
|
||||||
|
$imagePath = $image->storeAs('products', $imageName, 'public');
|
||||||
|
|
||||||
|
$this->image = $imagePath;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the product image
|
||||||
|
*/
|
||||||
|
public function deleteImage()
|
||||||
|
{
|
||||||
|
if ($this->image) {
|
||||||
|
Storage::disk('public')->delete($this->image);
|
||||||
|
$this->image = null;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full URL of the image
|
||||||
|
*/
|
||||||
|
public function getImageUrlAttribute()
|
||||||
|
{
|
||||||
|
if ($this->image) {
|
||||||
|
return Storage::url($this->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the model
|
||||||
|
*/
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::deleting(function ($product) {
|
||||||
|
// Delete the image when the product is deleted
|
||||||
|
$product->deleteImage();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
thanasoft-back/app/Models/ProductCategory.php
Normal file
102
thanasoft-back/app/Models/ProductCategory.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class ProductCategory extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'parent_id',
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent category
|
||||||
|
*/
|
||||||
|
public function parent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductCategory::class, 'parent_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the child categories
|
||||||
|
*/
|
||||||
|
public function children(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProductCategory::class, 'parent_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all products in this category
|
||||||
|
*/
|
||||||
|
public function products(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Product::class, 'categorie', 'name');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all descendant categories (recursive)
|
||||||
|
*/
|
||||||
|
public function descendants(): HasMany
|
||||||
|
{
|
||||||
|
return $this->children()->with('descendants');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hierarchical path of the category
|
||||||
|
*/
|
||||||
|
public function getPathAttribute(): string
|
||||||
|
{
|
||||||
|
$path = $this->name;
|
||||||
|
$parent = $this->parent;
|
||||||
|
|
||||||
|
while ($parent) {
|
||||||
|
$path = $parent->name . ' > ' . $path;
|
||||||
|
$parent = $parent->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get only active categories
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get root categories (no parent)
|
||||||
|
*/
|
||||||
|
public function scopeRoots($query)
|
||||||
|
{
|
||||||
|
return $query->whereNull('parent_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if category has children
|
||||||
|
*/
|
||||||
|
public function hasChildren(): bool
|
||||||
|
{
|
||||||
|
return $this->children()->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if category has products
|
||||||
|
*/
|
||||||
|
public function hasProducts(): bool
|
||||||
|
{
|
||||||
|
return $this->products()->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,6 +39,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->app->bind(\App\Repositories\ProductCategoryRepositoryInterface::class, function ($app) {
|
||||||
|
return new \App\Repositories\ProductCategoryRepository($app->make(\App\Models\ProductCategory::class));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
195
thanasoft-back/app/Repositories/ProductCategoryRepository.php
Normal file
195
thanasoft-back/app/Repositories/ProductCategoryRepository.php
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class ProductCategoryRepository implements ProductCategoryRepositoryInterface
|
||||||
|
{
|
||||||
|
protected $model;
|
||||||
|
|
||||||
|
public function __construct(ProductCategory $model)
|
||||||
|
{
|
||||||
|
$this->model = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all product categories with pagination
|
||||||
|
*/
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$query = $this->model->newQuery();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (isset($filters['search']) && !empty($filters['search'])) {
|
||||||
|
$search = $filters['search'];
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('code', 'like', "%{$search}%")
|
||||||
|
->orWhere('description', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($filters['active']) && $filters['active'] !== null) {
|
||||||
|
$query->where('active', $filters['active']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($filters['parent_id']) && $filters['parent_id'] !== null) {
|
||||||
|
$query->where('parent_id', $filters['parent_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
$sortBy = $filters['sort_by'] ?? 'name';
|
||||||
|
$sortDirection = $filters['sort_direction'] ?? 'asc';
|
||||||
|
$query->orderBy($sortBy, $sortDirection);
|
||||||
|
|
||||||
|
return $query->paginate($perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all product categories
|
||||||
|
*/
|
||||||
|
public function all(array $filters = []): Collection
|
||||||
|
{
|
||||||
|
$query = $this->model->newQuery();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (isset($filters['active']) && $filters['active'] !== null) {
|
||||||
|
$query->where('active', $filters['active']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($filters['parent_id']) && $filters['parent_id'] !== null) {
|
||||||
|
$query->where('parent_id', $filters['parent_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortBy = $filters['sort_by'] ?? 'name';
|
||||||
|
$sortDirection = $filters['sort_direction'] ?? 'asc';
|
||||||
|
$query->orderBy($sortBy, $sortDirection);
|
||||||
|
|
||||||
|
return $query->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a product category by ID
|
||||||
|
*/
|
||||||
|
public function find(string $id): ?ProductCategory
|
||||||
|
{
|
||||||
|
return $this->model->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new product category
|
||||||
|
*/
|
||||||
|
public function create(array $data): ProductCategory
|
||||||
|
{
|
||||||
|
return $this->model->create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a product category
|
||||||
|
*/
|
||||||
|
public function update(string $id, array $data): bool
|
||||||
|
{
|
||||||
|
$category = $this->find($id);
|
||||||
|
if ($category) {
|
||||||
|
return $category->update($data);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a product category
|
||||||
|
*/
|
||||||
|
public function delete(string $id): bool
|
||||||
|
{
|
||||||
|
$category = $this->find($id);
|
||||||
|
if ($category) {
|
||||||
|
return $category->delete();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active categories only
|
||||||
|
*/
|
||||||
|
public function getActive(): Collection
|
||||||
|
{
|
||||||
|
return $this->model->active()->orderBy('name')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get root categories (no parent)
|
||||||
|
*/
|
||||||
|
public function getRoots(): Collection
|
||||||
|
{
|
||||||
|
return $this->model->roots()->orderBy('name')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories with their children
|
||||||
|
*/
|
||||||
|
public function getWithChildren(): Collection
|
||||||
|
{
|
||||||
|
return $this->model->with('children')
|
||||||
|
->roots()
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search categories by name or code
|
||||||
|
*/
|
||||||
|
public function search(string $term, int $perPage = 15): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
return $this->model->where('name', 'like', "%{$term}%")
|
||||||
|
->orWhere('code', 'like', "%{$term}%")
|
||||||
|
->orWhere('description', 'like', "%{$term}%")
|
||||||
|
->orderBy('name')
|
||||||
|
->paginate($perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get category statistics
|
||||||
|
*/
|
||||||
|
public function getStatistics(): array
|
||||||
|
{
|
||||||
|
$total = $this->model->count();
|
||||||
|
$active = $this->model->active()->count();
|
||||||
|
$inactive = $total - $active;
|
||||||
|
$withChildren = $this->model->has('children')->count();
|
||||||
|
$rootCategories = $this->model->roots()->count();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_categories' => $total,
|
||||||
|
'active_categories' => $active,
|
||||||
|
'inactive_categories' => $inactive,
|
||||||
|
'categories_with_children' => $withChildren,
|
||||||
|
'root_categories' => $rootCategories,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if category can be deleted
|
||||||
|
*/
|
||||||
|
public function canDelete(string $id): bool
|
||||||
|
{
|
||||||
|
$category = $this->find($id);
|
||||||
|
if (!$category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete if has children
|
||||||
|
if ($category->hasChildren()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete if has products
|
||||||
|
if ($category->hasProducts()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
interface ProductCategoryRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all product categories with pagination
|
||||||
|
*/
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all product categories
|
||||||
|
*/
|
||||||
|
public function all(array $filters = []): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a product category by ID
|
||||||
|
*/
|
||||||
|
public function find(string $id): ?ProductCategory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new product category
|
||||||
|
*/
|
||||||
|
public function create(array $data): ProductCategory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a product category
|
||||||
|
*/
|
||||||
|
public function update(string $id, array $data): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a product category
|
||||||
|
*/
|
||||||
|
public function delete(string $id): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active categories only
|
||||||
|
*/
|
||||||
|
public function getActive(): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get root categories (no parent)
|
||||||
|
*/
|
||||||
|
public function getRoots(): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get categories with their children
|
||||||
|
*/
|
||||||
|
public function getWithChildren(): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search categories by name or code
|
||||||
|
*/
|
||||||
|
public function search(string $term, int $perPage = 15): LengthAwarePaginator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get category statistics
|
||||||
|
*/
|
||||||
|
public function getStatistics(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if category can be deleted
|
||||||
|
*/
|
||||||
|
public function canDelete(string $id): bool;
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?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::table('products', function (Blueprint $table) {
|
||||||
|
// Remove the old photo_url column
|
||||||
|
$table->dropColumn('photo_url');
|
||||||
|
|
||||||
|
// Add new image column for file uploads
|
||||||
|
$table->string('image')->nullable()->after('conditionnement_unite');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('products', function (Blueprint $table) {
|
||||||
|
// Remove the image column
|
||||||
|
$table->dropColumn('image');
|
||||||
|
|
||||||
|
// Restore the old photo_url column
|
||||||
|
$table->string('photo_url')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<?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('product_categories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('parent_id')->nullable();
|
||||||
|
$table->string('code', 64);
|
||||||
|
$table->string('name', 191);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->boolean('active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('parent_id')
|
||||||
|
->references('id')
|
||||||
|
->on('product_categories')
|
||||||
|
->onDelete('set null');
|
||||||
|
|
||||||
|
$table->index('code');
|
||||||
|
$table->index('active');
|
||||||
|
$table->index('parent_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('product_categories');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -19,5 +19,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
'name' => 'Test User',
|
'name' => 'Test User',
|
||||||
'email' => 'test@example.com',
|
'email' => 'test@example.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->call(ProductCategorySeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
thanasoft-back/database/seeders/ProductCategorySeeder.php
Normal file
101
thanasoft-back/database/seeders/ProductCategorySeeder.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\ProductCategory;
|
||||||
|
|
||||||
|
class ProductCategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Create root categories
|
||||||
|
$electronique = ProductCategory::create([
|
||||||
|
'code' => 'ELECTRONIQUE',
|
||||||
|
'name' => 'Électronique',
|
||||||
|
'description' => 'Produits électroniques et technologiques',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$alimentaire = ProductCategory::create([
|
||||||
|
'code' => 'ALIMENTAIRE',
|
||||||
|
'name' => 'Alimentaire',
|
||||||
|
'description' => 'Produits alimentaires et boissons',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$medical = ProductCategory::create([
|
||||||
|
'code' => 'MEDICAL',
|
||||||
|
'name' => 'Médical',
|
||||||
|
'description' => 'Produits médicaux et pharmaceutiques',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create subcategories
|
||||||
|
ProductCategory::create([
|
||||||
|
'parent_id' => $electronique->id,
|
||||||
|
'code' => 'TELEPHONE',
|
||||||
|
'name' => 'Téléphones',
|
||||||
|
'description' => 'Smartphones et téléphones mobiles',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductCategory::create([
|
||||||
|
'parent_id' => $electronique->id,
|
||||||
|
'code' => 'ORDINATEUR',
|
||||||
|
'name' => 'Ordinateurs',
|
||||||
|
'description' => 'Ordinateurs portables et de bureau',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductCategory::create([
|
||||||
|
'parent_id' => $alimentaire->id,
|
||||||
|
'code' => 'FRUITS',
|
||||||
|
'name' => 'Fruits',
|
||||||
|
'description' => 'Fruits frais et transformés',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductCategory::create([
|
||||||
|
'parent_id' => $alimentaire->id,
|
||||||
|
'code' => 'LEGUMES',
|
||||||
|
'name' => 'Légumes',
|
||||||
|
'description' => 'Légumes frais et transformés',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductCategory::create([
|
||||||
|
'parent_id' => $medical->id,
|
||||||
|
'code' => 'MEDICAMENT',
|
||||||
|
'name' => 'Médicaments',
|
||||||
|
'description' => 'Médicaments et traitements',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductCategory::create([
|
||||||
|
'parent_id' => $medical->id,
|
||||||
|
'code' => 'MATERIEL',
|
||||||
|
'name' => 'Matériel Médical',
|
||||||
|
'description' => 'Équipements et instruments médicaux',
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create some inactive categories for testing
|
||||||
|
ProductCategory::create([
|
||||||
|
'code' => 'COSMETIQUE',
|
||||||
|
'name' => 'Cosmétique',
|
||||||
|
'description' => 'Produits cosmétiques et de beauté',
|
||||||
|
'active' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductCategory::create([
|
||||||
|
'code' => 'MENAGE',
|
||||||
|
'name' => 'Ménage',
|
||||||
|
'description' => 'Produits d\'entretien et de nettoyage',
|
||||||
|
'active' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ 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\ProductController;
|
use App\Http\Controllers\Api\ProductController;
|
||||||
|
use App\Http\Controllers\Api\ProductCategoryController;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -63,4 +64,13 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::get('/products/statistics', [ProductController::class, 'statistics']);
|
Route::get('/products/statistics', [ProductController::class, 'statistics']);
|
||||||
Route::apiResource('products', ProductController::class);
|
Route::apiResource('products', ProductController::class);
|
||||||
Route::patch('/products/{id}/stock', [ProductController::class, 'updateStock']);
|
Route::patch('/products/{id}/stock', [ProductController::class, 'updateStock']);
|
||||||
|
|
||||||
|
// Product Category management
|
||||||
|
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
||||||
|
Route::get('/product-categories/active', [ProductCategoryController::class, 'active']);
|
||||||
|
Route::get('/product-categories/roots', [ProductCategoryController::class, 'roots']);
|
||||||
|
Route::get('/product-categories/hierarchical', [ProductCategoryController::class, 'hierarchical']);
|
||||||
|
Route::get('/product-categories/statistics', [ProductCategoryController::class, 'statistics']);
|
||||||
|
Route::apiResource('product-categories', ProductCategoryController::class);
|
||||||
|
Route::patch('/product-categories/{id}/toggle-active', [ProductCategoryController::class, 'toggleActive']);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user