diff --git a/thanasoft-back/app/Http/Controllers/Api/StockItemController.php b/thanasoft-back/app/Http/Controllers/Api/StockItemController.php new file mode 100644 index 0000000..dc0ca61 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/StockItemController.php @@ -0,0 +1,134 @@ +stockItemRepository->all(); + return response()->json([ + 'data' => StockItemResource::collection($items), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock items: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des stocks.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created stock item. + */ + public function store(StoreStockItemRequest $request): JsonResponse + { + try { + $item = $this->stockItemRepository->create($request->validated()); + return response()->json([ + 'data' => new StockItemResource($item), + 'message' => 'Stock initialisé avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'initialisation du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified stock item. + */ + public function show(string $id): JsonResponse + { + try { + $item = $this->stockItemRepository->find((int) $id); + if (!$item) { + return response()->json(['message' => 'Stock non trouvé.'], 404); + } + return response()->json([ + 'data' => new StockItemResource($item), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified stock item. + */ + public function update(UpdateStockItemRequest $request, string $id): JsonResponse + { + try { + $updated = $this->stockItemRepository->update((int) $id, $request->validated()); + if (!$updated) { + return response()->json(['message' => 'Stock non trouvé ou échec de la mise à jour.'], 404); + } + $item = $this->stockItemRepository->find((int) $id); + return response()->json([ + 'data' => new StockItemResource($item), + 'message' => 'Stock mis à jour avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error updating stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified stock item. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->stockItemRepository->delete((int) $id); + if (!$deleted) { + return response()->json(['message' => 'Stock non trouvé ou échec de la suppression.'], 404); + } + return response()->json([ + 'message' => 'Stock supprimé avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error deleting stock item: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/StockMoveController.php b/thanasoft-back/app/Http/Controllers/Api/StockMoveController.php new file mode 100644 index 0000000..55fe5bf --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/StockMoveController.php @@ -0,0 +1,85 @@ +stockMoveRepository->all(); + return response()->json([ + 'data' => StockMoveResource::collection($moves), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock moves: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des mouvements de stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created stock move. + */ + public function store(StoreStockMoveRequest $request): JsonResponse + { + try { + $move = $this->stockMoveRepository->create($request->validated()); + return response()->json([ + 'data' => new StockMoveResource($move), + 'message' => 'Mouvement de stock enregistré avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating stock move: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'enregistrement du mouvement de stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified stock move. + */ + public function show(string $id): JsonResponse + { + try { + $move = $this->stockMoveRepository->find((int) $id); + if (!$move) { + return response()->json(['message' => 'Mouvement de stock non trouvé.'], 404); + } + return response()->json([ + 'data' => new StockMoveResource($move), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching stock move: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du mouvement de stock.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/WarehouseController.php b/thanasoft-back/app/Http/Controllers/Api/WarehouseController.php new file mode 100644 index 0000000..56abd83 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/WarehouseController.php @@ -0,0 +1,134 @@ +warehouseRepository->all(); + return response()->json([ + 'data' => WarehouseResource::collection($warehouses), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching warehouses: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des entrepôts.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created warehouse. + */ + public function store(StoreWarehouseRequest $request): JsonResponse + { + try { + $warehouse = $this->warehouseRepository->create($request->validated()); + return response()->json([ + 'data' => new WarehouseResource($warehouse), + 'message' => 'Entrepôt créé avec succès.', + 'status' => 'success' + ], 201); + } catch (\Exception $e) { + Log::error('Error creating warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified warehouse. + */ + public function show(string $id): JsonResponse + { + try { + $warehouse = $this->warehouseRepository->find((int) $id); + if (!$warehouse) { + return response()->json(['message' => 'Entrepôt non trouvé.'], 404); + } + return response()->json([ + 'data' => new WarehouseResource($warehouse), + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error fetching warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified warehouse. + */ + public function update(UpdateWarehouseRequest $request, string $id): JsonResponse + { + try { + $updated = $this->warehouseRepository->update((int) $id, $request->validated()); + if (!$updated) { + return response()->json(['message' => 'Entrepôt non trouvé ou échec de la mise à jour.'], 404); + } + $warehouse = $this->warehouseRepository->find((int) $id); + return response()->json([ + 'data' => new WarehouseResource($warehouse), + 'message' => 'Entrepôt mis à jour avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error updating warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified warehouse. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->warehouseRepository->delete((int) $id); + if (!$deleted) { + return response()->json(['message' => 'Entrepôt non trouvé ou échec de la suppression.'], 404); + } + return response()->json([ + 'message' => 'Entrepôt supprimé avec succès.', + 'status' => 'success' + ]); + } catch (\Exception $e) { + Log::error('Error deleting warehouse: ' . $e->getMessage()); + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'entrepôt.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreStockItemRequest.php b/thanasoft-back/app/Http/Requests/StoreStockItemRequest.php new file mode 100644 index 0000000..b9b1b12 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreStockItemRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + return [ + 'product_id' => 'required|exists:products,id', + 'warehouse_id' => 'required|exists:warehouses,id', + 'qty_on_hand_base' => 'nullable|numeric|min:0', + 'safety_stock_base' => 'nullable|numeric|min:0', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'product_id.required' => 'Le produit est requis.', + 'product_id.exists' => 'Le produit sélectionné est invalide.', + 'warehouse_id.required' => 'L\'entrepôt est requis.', + 'warehouse_id.exists' => 'L\'entrepôt sélectionné est invalide.', + 'qty_on_hand_base.numeric' => 'La quantité en stock doit être un nombre.', + 'safety_stock_base.numeric' => 'Le stock de sécurité doit être un nombre.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php b/thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php new file mode 100644 index 0000000..3d69eab --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php @@ -0,0 +1,58 @@ +|string> + */ + public function rules(): array + { + return [ + 'product_id' => 'required|exists:products,id', + 'from_warehouse_id' => 'nullable|exists:warehouses,id', + 'to_warehouse_id' => 'nullable|exists:warehouses,id', + 'packaging_id' => 'nullable|exists:product_packagings,id', + 'packages_qty' => 'nullable|numeric|min:0', + 'units_qty' => 'nullable|numeric|min:0', + 'qty_base' => 'required|numeric', + 'move_type' => 'required|string|max:64', + 'ref_type' => 'nullable|string|max:64', + 'ref_id' => 'nullable|integer', + 'moved_at' => 'nullable|date', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'product_id.required' => 'Le produit est requis.', + 'product_id.exists' => 'Le produit sélectionné est invalide.', + 'from_warehouse_id.exists' => 'L\'entrepôt de départ est invalide.', + 'to_warehouse_id.exists' => 'L\'entrepôt d\'arrivée est invalide.', + 'packaging_id.exists' => 'Le conditionnement sélectionné est invalide.', + 'qty_base.required' => 'La quantité de base est requise.', + 'qty_base.numeric' => 'La quantité de base doit être un nombre.', + 'move_type.required' => 'Le type de mouvement est requis.', + 'moved_at.date' => 'La date du mouvement n\'est pas valide.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php b/thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php new file mode 100644 index 0000000..a2c9c9a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php @@ -0,0 +1,52 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Le nom de l\'entrepôt est requis.', + 'name.string' => 'Le nom doit être une chaîne de caractères.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit comporter exactement 2 caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php b/thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php new file mode 100644 index 0000000..c6a2466 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php @@ -0,0 +1,42 @@ +|string> + */ + public function rules(): array + { + return [ + 'qty_on_hand_base' => 'sometimes|numeric|min:0', + 'safety_stock_base' => 'sometimes|numeric|min:0', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'qty_on_hand_base.numeric' => 'La quantité en stock doit être un nombre.', + 'safety_stock_base.numeric' => 'Le stock de sécurité doit être un nombre.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php b/thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php new file mode 100644 index 0000000..153d147 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php @@ -0,0 +1,52 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'sometimes|required|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Le nom de l\'entrepôt est requis.', + 'name.string' => 'Le nom doit être une chaîne de caractères.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit comporter exactement 2 caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/ProductPackagingResource.php b/thanasoft-back/app/Http/Resources/ProductPackagingResource.php new file mode 100644 index 0000000..65d54e4 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ProductPackagingResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'product_id' => $this->product_id, + 'name' => $this->name, + 'qty_base' => $this->qty_base, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/StockItemResource.php b/thanasoft-back/app/Http/Resources/StockItemResource.php new file mode 100644 index 0000000..161084e --- /dev/null +++ b/thanasoft-back/app/Http/Resources/StockItemResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'product_id' => $this->product_id, + 'warehouse_id' => $this->warehouse_id, + 'qty_on_hand_base' => $this->qty_on_hand_base, + 'safety_stock_base' => $this->safety_stock_base, + 'product' => new ProductResource($this->whenLoaded('product')), + 'warehouse' => new WarehouseResource($this->whenLoaded('warehouse')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/StockMoveResource.php b/thanasoft-back/app/Http/Resources/StockMoveResource.php new file mode 100644 index 0000000..a9559df --- /dev/null +++ b/thanasoft-back/app/Http/Resources/StockMoveResource.php @@ -0,0 +1,38 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'product_id' => $this->product_id, + 'from_warehouse_id' => $this->from_warehouse_id, + 'to_warehouse_id' => $this->to_warehouse_id, + 'packaging_id' => $this->packaging_id, + 'packages_qty' => $this->packages_qty, + 'units_qty' => $this->units_qty, + 'qty_base' => $this->qty_base, + 'move_type' => $this->move_type, + 'ref_type' => $this->ref_type, + 'ref_id' => $this->ref_id, + 'moved_at' => $this->moved_at, + 'product' => new ProductResource($this->whenLoaded('product')), + 'from_warehouse' => new WarehouseResource($this->whenLoaded('fromWarehouse')), + 'to_warehouse' => new WarehouseResource($this->whenLoaded('toWarehouse')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/WarehouseResource.php b/thanasoft-back/app/Http/Resources/WarehouseResource.php new file mode 100644 index 0000000..d5487b2 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/WarehouseResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'address_line1' => $this->address_line1, + 'address_line2' => $this->address_line2, + 'postal_code' => $this->postal_code, + 'city' => $this->city, + 'country_code' => $this->country_code, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Models/Product.php b/thanasoft-back/app/Models/Product.php index fe332cc..895352e 100644 --- a/thanasoft-back/app/Models/Product.php +++ b/thanasoft-back/app/Models/Product.php @@ -99,6 +99,30 @@ class Product extends Model return null; } + /** + * Get the stock items for the product. + */ + public function stockItems(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(StockItem::class); + } + + /** + * Get the packagings for the product. + */ + public function packagings(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(\App\Models\Stock\ProductPackaging::class); + } + + /** + * Get the stock moves for the product. + */ + public function stockMoves(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(StockMove::class); + } + /** * Boot the model */ diff --git a/thanasoft-back/app/Models/Stock/ProductPackaging.php b/thanasoft-back/app/Models/Stock/ProductPackaging.php new file mode 100644 index 0000000..0eac92f --- /dev/null +++ b/thanasoft-back/app/Models/Stock/ProductPackaging.php @@ -0,0 +1,30 @@ + 'decimal:3', + ]; + + /** + * Get the product that owns the packaging. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } +} diff --git a/thanasoft-back/app/Models/StockItem.php b/thanasoft-back/app/Models/StockItem.php new file mode 100644 index 0000000..e16624f --- /dev/null +++ b/thanasoft-back/app/Models/StockItem.php @@ -0,0 +1,39 @@ + 'decimal:3', + 'safety_stock_base' => 'decimal:3', + ]; + + /** + * Get the product associated with this stock item. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * Get the warehouse where this stock item is located. + */ + public function warehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class); + } +} diff --git a/thanasoft-back/app/Models/StockMove.php b/thanasoft-back/app/Models/StockMove.php new file mode 100644 index 0000000..937a5c2 --- /dev/null +++ b/thanasoft-back/app/Models/StockMove.php @@ -0,0 +1,65 @@ + 'decimal:3', + 'units_qty' => 'decimal:3', + 'qty_base' => 'decimal:3', + 'moved_at' => 'datetime', + ]; + + /** + * Get the product being moved. + */ + public function product(): BelongsTo + { + return $this->belongsTo(Product::class); + } + + /** + * Get the source warehouse. + */ + public function fromWarehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class, 'from_warehouse_id'); + } + + /** + * Get the destination warehouse. + */ + public function toWarehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class, 'to_warehouse_id'); + } + + /** + * Get the packaging used for this move. + */ + public function packaging(): BelongsTo + { + return $this->belongsTo(ProductPackaging::class, 'packaging_id'); + } +} diff --git a/thanasoft-back/app/Models/Warehouse.php b/thanasoft-back/app/Models/Warehouse.php new file mode 100644 index 0000000..6559b8c --- /dev/null +++ b/thanasoft-back/app/Models/Warehouse.php @@ -0,0 +1,44 @@ +hasMany(StockItem::class); + } + + /** + * Get the stock moves from this warehouse. + */ + public function movesFrom(): HasMany + { + return $this->hasMany(StockMove::class, 'from_warehouse_id'); + } + + /** + * Get the stock moves to this warehouse. + */ + public function movesTo(): HasMany + { + return $this->hasMany(StockMove::class, 'to_warehouse_id'); + } +} diff --git a/thanasoft-back/app/Providers/RepositoryServiceProvider.php b/thanasoft-back/app/Providers/RepositoryServiceProvider.php index cf70563..03d0595 100644 --- a/thanasoft-back/app/Providers/RepositoryServiceProvider.php +++ b/thanasoft-back/app/Providers/RepositoryServiceProvider.php @@ -24,7 +24,10 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class); $this->app->bind(FileRepositoryInterface::class, FileRepository::class); $this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class); - + $this->app->bind(\App\Repositories\WarehouseRepositoryInterface::class, \App\Repositories\WarehouseRepository::class); + $this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class); + $this->app->bind(\App\Repositories\StockMoveRepositoryInterface::class, \App\Repositories\StockMoveRepository::class); + $this->app->bind(\App\Repositories\ProductPackagingRepositoryInterface::class, \App\Repositories\ProductPackagingRepository::class); } /** diff --git a/thanasoft-back/app/Repositories/ProductPackagingRepository.php b/thanasoft-back/app/Repositories/ProductPackagingRepository.php new file mode 100644 index 0000000..8300684 --- /dev/null +++ b/thanasoft-back/app/Repositories/ProductPackagingRepository.php @@ -0,0 +1,15 @@ + + */ +class ProductPackagingFactory extends Factory +{ + protected $model = ProductPackaging::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'name' => $this->faker->word(), + 'qty_base' => $this->faker->randomFloat(3, 1, 100), + ]; + } +} diff --git a/thanasoft-back/database/factories/StockItemFactory.php b/thanasoft-back/database/factories/StockItemFactory.php new file mode 100644 index 0000000..eb46adc --- /dev/null +++ b/thanasoft-back/database/factories/StockItemFactory.php @@ -0,0 +1,31 @@ + + */ +class StockItemFactory extends Factory +{ + protected $model = StockItem::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'warehouse_id' => Warehouse::factory(), + 'qty_on_hand_base' => $this->faker->randomFloat(3, 0, 1000), + 'safety_stock_base' => $this->faker->randomFloat(3, 0, 100), + ]; + } +} diff --git a/thanasoft-back/database/factories/StockMoveFactory.php b/thanasoft-back/database/factories/StockMoveFactory.php new file mode 100644 index 0000000..103f2a9 --- /dev/null +++ b/thanasoft-back/database/factories/StockMoveFactory.php @@ -0,0 +1,33 @@ + + */ +class StockMoveFactory extends Factory +{ + protected $model = StockMove::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'product_id' => Product::factory(), + 'from_warehouse_id' => Warehouse::factory(), + 'to_warehouse_id' => Warehouse::factory(), + 'qty_base' => $this->faker->randomFloat(3, 1, 100), + 'move_type' => $this->faker->randomElement(['receipt', 'issue', 'transfer', 'adjustment', 'consumption']), + 'moved_at' => now(), + ]; + } +} diff --git a/thanasoft-back/database/factories/WarehouseFactory.php b/thanasoft-back/database/factories/WarehouseFactory.php new file mode 100644 index 0000000..5eaeb2d --- /dev/null +++ b/thanasoft-back/database/factories/WarehouseFactory.php @@ -0,0 +1,31 @@ + + */ +class WarehouseFactory extends Factory +{ + protected $model = Warehouse::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company() . ' Warehouse', + 'address_line1' => $this->faker->streetAddress(), + 'address_line2' => $this->faker->secondaryAddress(), + 'postal_code' => $this->faker->postcode(), + 'city' => $this->faker->city(), + 'country_code' => 'FR', + ]; + } +} diff --git a/thanasoft-back/database/migrations/2026_02_02_153000_create_warehouses_table.php b/thanasoft-back/database/migrations/2026_02_02_153000_create_warehouses_table.php new file mode 100644 index 0000000..015a336 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153000_create_warehouses_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name', 191); + $table->string('address_line1', 255)->nullable(); + $table->string('address_line2', 255)->nullable(); + $table->string('postal_code', 20)->nullable(); + $table->string('city', 191)->nullable(); + $table->char('country_code', 2)->default('FR'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('warehouses'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153001_create_product_packagings_table.php b/thanasoft-back/database/migrations/2026_02_02_153001_create_product_packagings_table.php new file mode 100644 index 0000000..7230b80 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153001_create_product_packagings_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('product_id')->constrained()->onDelete('cascade'); + $table->string('name', 191); + $table->decimal('qty_base', 14, 3); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_packagings'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153002_create_stock_items_table.php b/thanasoft-back/database/migrations/2026_02_02_153002_create_stock_items_table.php new file mode 100644 index 0000000..bcfbe73 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153002_create_stock_items_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('product_id')->constrained()->onDelete('cascade'); + $table->foreignId('warehouse_id')->constrained()->onDelete('cascade'); + $table->decimal('qty_on_hand_base', 14, 3)->default(0); + $table->decimal('safety_stock_base', 14, 3)->default(0); + $table->timestamps(); + + $table->unique(['product_id', 'warehouse_id'], 'uq_stock_item'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stock_items'); + } +}; diff --git a/thanasoft-back/database/migrations/2026_02_02_153003_create_stock_moves_table.php b/thanasoft-back/database/migrations/2026_02_02_153003_create_stock_moves_table.php new file mode 100644 index 0000000..3a1b0e3 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_02_02_153003_create_stock_moves_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('product_id')->constrained()->onDelete('cascade'); + $table->foreignId('from_warehouse_id')->nullable()->constrained('warehouses')->onDelete('set null'); + $table->foreignId('to_warehouse_id')->nullable()->constrained('warehouses')->onDelete('set null'); + $table->foreignId('packaging_id')->nullable()->constrained('product_packagings')->onDelete('set null'); + $table->decimal('packages_qty', 14, 3)->nullable(); + $table->decimal('units_qty', 14, 3)->nullable(); + $table->decimal('qty_base', 14, 3); + $table->string('move_type', 64); + $table->string('ref_type', 64)->nullable(); + $table->unsignedBigInteger('ref_id')->nullable(); + $table->timestamp('moved_at')->useCurrent(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stock_moves'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index d7b9ba5..3525b07 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -98,6 +98,13 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('products', ProductController::class); Route::patch('/products/{id}/stock', [ProductController::class, 'updateStock']); + // Warehouse management + Route::apiResource('warehouses', \App\Http\Controllers\Api\WarehouseController::class); + + // Stock management + Route::apiResource('stock-items', \App\Http\Controllers\Api\StockItemController::class); + Route::apiResource('stock-moves', \App\Http\Controllers\Api\StockMoveController::class); + // Product Category management Route::get('/product-categories/search', [ProductCategoryController::class, 'search']); Route::get('/product-categories/active', [ProductCategoryController::class, 'active']); @@ -124,7 +131,7 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('practitioner-documents', PractitionerDocumentController::class); Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']); - // Deceased Routes + // Deceased Routes Route::prefix('deceased')->group(function () { Route::get('/searchBy', [DeceasedController::class, 'searchBy']); Route::get('/', [DeceasedController::class, 'index']); diff --git a/thanasoft-back/tests/Feature/StockMoveApiTest.php b/thanasoft-back/tests/Feature/StockMoveApiTest.php new file mode 100644 index 0000000..2f7b252 --- /dev/null +++ b/thanasoft-back/tests/Feature/StockMoveApiTest.php @@ -0,0 +1,65 @@ +create()); + } + + public function test_can_list_stock_moves(): void + { + StockMove::factory()->count(2)->create(); + + $response = $this->getJson('/api/stock-moves'); + + $response->assertStatus(200) + ->assertJsonCount(2, 'data'); + } + + public function test_can_create_stock_move(): void + { + $product = Product::factory()->create(); + $warehouse = Warehouse::factory()->create(); + + $data = [ + 'product_id' => $product->id, + 'to_warehouse_id' => $warehouse->id, + 'qty_base' => 10, + 'move_type' => 'receipt', + 'moved_at' => now()->toDateTimeString(), + ]; + + $response = $this->postJson('/api/stock-moves', $data); + + $response->assertStatus(201) + ->assertJsonPath('data.qty_base', 10); + + $this->assertDatabaseHas('stock_moves', [ + 'product_id' => $product->id, + 'qty_base' => 10 + ]); + } + + public function test_valide_french_messages_for_stock_move(): void + { + $response = $this->postJson('/api/stock-moves', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['product_id', 'qty_base', 'move_type']) + ->assertJsonFragment(['product_id' => ['Le produit est requis.']]); + } +} diff --git a/thanasoft-back/tests/Feature/WarehouseApiTest.php b/thanasoft-back/tests/Feature/WarehouseApiTest.php new file mode 100644 index 0000000..db5ba72 --- /dev/null +++ b/thanasoft-back/tests/Feature/WarehouseApiTest.php @@ -0,0 +1,89 @@ +create()); + } + + public function test_can_list_warehouses(): void + { + Warehouse::factory()->count(3)->create(); + + $response = $this->getJson('/api/warehouses'); + + $response->assertStatus(200) + ->assertJsonCount(3, 'data'); + } + + public function test_can_create_warehouse(): void + { + $data = [ + 'name' => 'Main Warehouse', + 'city' => 'Paris', + 'country_code' => 'FR', + ]; + + $response = $this->postJson('/api/warehouses', $data); + + $response->assertStatus(201) + ->assertJsonPath('data.name', 'Main Warehouse'); + + $this->assertDatabaseHas('warehouses', ['name' => 'Main Warehouse']); + } + + public function test_can_show_warehouse(): void + { + $warehouse = Warehouse::factory()->create(); + + $response = $this->getJson('/api/warehouses/' . $warehouse->id); + + $response->assertStatus(200) + ->assertJsonPath('data.id', $warehouse->id); + } + + public function test_can_update_warehouse(): void + { + $warehouse = Warehouse::factory()->create(['name' => 'Old Name']); + + $response = $this->putJson('/api/warehouses/' . $warehouse->id, [ + 'name' => 'New Name' + ]); + + $response->assertStatus(200) + ->assertJsonPath('data.name', 'New Name'); + + $this->assertDatabaseHas('warehouses', ['id' => $warehouse->id, 'name' => 'New Name']); + } + + public function test_can_delete_warehouse(): void + { + $warehouse = Warehouse::factory()->create(); + + $response = $this->deleteJson('/api/warehouses/' . $warehouse->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('warehouses', ['id' => $warehouse->id]); + } + + public function test_valide_french_messages(): void + { + $response = $this->postJson('/api/warehouses', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['name']) + ->assertJsonFragment(['name' => ['Le nom de l\'entrepôt est requis.']]); + } +} diff --git a/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue b/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue new file mode 100644 index 0000000..bdd5160 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue @@ -0,0 +1,95 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/WarehouseFormPresentation.vue b/thanasoft-front/src/components/Organism/Stock/WarehouseFormPresentation.vue new file mode 100644 index 0000000..1623f68 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/WarehouseFormPresentation.vue @@ -0,0 +1,96 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/WarehouseListPresentation.vue b/thanasoft-front/src/components/Organism/Stock/WarehouseListPresentation.vue new file mode 100644 index 0000000..1578f6e --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/WarehouseListPresentation.vue @@ -0,0 +1,65 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Stock/WarehouseDetailInfo.vue b/thanasoft-front/src/components/molecules/Stock/WarehouseDetailInfo.vue new file mode 100644 index 0000000..852c729 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Stock/WarehouseDetailInfo.vue @@ -0,0 +1,33 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue b/thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue new file mode 100644 index 0000000..b2db630 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue @@ -0,0 +1,111 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Stock/WarehouseTable.vue b/thanasoft-front/src/components/molecules/Tables/Stock/WarehouseTable.vue new file mode 100644 index 0000000..4f5ea4f --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Stock/WarehouseTable.vue @@ -0,0 +1,168 @@ + + + diff --git a/thanasoft-front/src/examples/Sidenav/SidenavList.vue b/thanasoft-front/src/examples/Sidenav/SidenavList.vue index ea490e8..3d22fc7 100644 --- a/thanasoft-front/src/examples/Sidenav/SidenavList.vue +++ b/thanasoft-front/src/examples/Sidenav/SidenavList.vue @@ -319,6 +319,12 @@ export default { miniIcon: "S", text: "Stock", }, + { + id: "warehouses", + route: { name: "Entrepôts" }, + miniIcon: "E", + text: "Entrepôts", + }, ], }, { diff --git a/thanasoft-front/src/router/index.js b/thanasoft-front/src/router/index.js index 8e97ace..8bc929e 100644 --- a/thanasoft-front/src/router/index.js +++ b/thanasoft-front/src/router/index.js @@ -614,6 +614,27 @@ const routes = [ name: "Product details", component: () => import("@/views/pages/Stock/ProductDetails.vue"), }, + // Warehouses + { + path: "/stock/warehouses", + name: "Entrepôts", + component: () => import("@/views/pages/Stock/WarehouseList.vue"), + }, + { + path: "/stock/warehouses/new", + name: "Nouvel Entrepôt", + component: () => import("@/views/pages/Stock/NewWarehouse.vue"), + }, + { + path: "/stock/warehouses/:id", + name: "Détails Entrepôt", + component: () => import("@/views/pages/Stock/WarehouseDetail.vue"), + }, + { + path: "/stock/warehouses/:id/edit", + name: "Modifier Entrepôt", + component: () => import("@/views/pages/Stock/EditWarehouse.vue"), + }, // Employés { path: "/employes", diff --git a/thanasoft-front/src/services/productPackaging.ts b/thanasoft-front/src/services/productPackaging.ts new file mode 100644 index 0000000..ae8cc68 --- /dev/null +++ b/thanasoft-front/src/services/productPackaging.ts @@ -0,0 +1,51 @@ +import { request } from "./http"; + +export interface ProductPackaging { + id: number; + product_id: number; + name: string; + qty_base: number; + created_at: string; + updated_at: string; +} + +class ProductPackagingService { + async getAllPackagings(): Promise<{ data: ProductPackaging[] }> { + return await request<{ data: ProductPackaging[] }>({ + url: "/api/product-packagings", + method: "get", + }); + } + + async getPackaging(id: number): Promise<{ data: ProductPackaging }> { + return await request<{ data: ProductPackaging }>({ + url: `/api/product-packagings/${id}`, + method: "get", + }); + } + + async createPackaging(data: any): Promise<{ data: ProductPackaging }> { + return await request<{ data: ProductPackaging }>({ + url: "/api/product-packagings", + method: "post", + data, + }); + } + + async updatePackaging(id: number, data: any): Promise<{ data: ProductPackaging }> { + return await request<{ data: ProductPackaging }>({ + url: `/api/product-packagings/${id}`, + method: "put", + data, + }); + } + + async deletePackaging(id: number): Promise<{ message: string }> { + return await request<{ message: string }>({ + url: `/api/product-packagings/${id}`, + method: "delete", + }); + } +} + +export default ProductPackagingService; diff --git a/thanasoft-front/src/services/stock.ts b/thanasoft-front/src/services/stock.ts new file mode 100644 index 0000000..defb295 --- /dev/null +++ b/thanasoft-front/src/services/stock.ts @@ -0,0 +1,91 @@ +import { request } from "./http"; + +export interface StockItem { + id: number; + product_id: number; + warehouse_id: number; + qty_on_hand_base: number; + safety_stock_base: number; + product?: any; + warehouse?: any; + created_at: string; + updated_at: string; +} + +export interface StockMove { + id: number; + product_id: number; + from_warehouse_id: number | null; + to_warehouse_id: number | null; + packaging_id: number | null; + packages_qty: number | null; + units_qty: number | null; + qty_base: number; + move_type: string; + ref_type: string | null; + ref_id: number | null; + moved_at: string; + product?: any; + from_warehouse?: any; + to_warehouse?: any; + created_at: string; + updated_at: string; +} + +class StockService { + // Stock Items + async getAllStockItems(): Promise<{ data: StockItem[] }> { + return await request<{ data: StockItem[] }>({ + url: "/api/stock-items", + method: "get", + }); + } + + async getStockItem(id: number): Promise<{ data: StockItem }> { + return await request<{ data: StockItem }>({ + url: `/api/stock-items/${id}`, + method: "get", + }); + } + + async createStockItem(data: any): Promise<{ data: StockItem }> { + return await request<{ data: StockItem }>({ + url: "/api/stock-items", + method: "post", + data, + }); + } + + async updateStockItem(id: number, data: any): Promise<{ data: StockItem }> { + return await request<{ data: StockItem }>({ + url: `/api/stock-items/${id}`, + method: "put", + data, + }); + } + + // Stock Moves + async getAllStockMoves(): Promise<{ data: StockMove[] }> { + return await request<{ data: StockMove[] }>({ + url: "/api/stock-moves", + method: "get", + }); + } + + async getStockMove(id: number): Promise<{ data: StockMove }> { + return await request<{ data: StockMove }>({ + url: `/api/stock-moves/${id}`, + method: "get", + }); + } + + async createStockMove(data: any): Promise<{ data: StockMove }> { + return await request<{ data: StockMove }>({ + url: "/api/stock-moves", + method: "post", + data, + }); + } +} + +export default StockService; diff --git a/thanasoft-front/src/services/warehouse.ts b/thanasoft-front/src/services/warehouse.ts new file mode 100644 index 0000000..ecfdc3e --- /dev/null +++ b/thanasoft-front/src/services/warehouse.ts @@ -0,0 +1,63 @@ +import { request } from "./http"; + +export interface Warehouse { + id: number; + name: string; + address_line1: string | null; + address_line2: string | null; + postal_code: string | null; + city: string | null; + country_code: string; + created_at: string; + updated_at: string; +} + +export interface WarehouseFormData { + name: string; + address_line1?: string | null; + address_line2?: string | null; + postal_code?: string | null; + city?: string | null; + country_code?: string; +} + +class WarehouseService { + async getAllWarehouses(): Promise<{ data: Warehouse[] }> { + return await request<{ data: Warehouse[] }>({ + url: "/api/warehouses", + method: "get", + }); + } + + async getWarehouse(id: number): Promise<{ data: Warehouse }> { + return await request<{ data: Warehouse }>({ + url: `/api/warehouses/${id}`, + method: "get", + }); + } + + async createWarehouse(data: WarehouseFormData): Promise<{ data: Warehouse }> { + return await request<{ data: Warehouse }>({ + url: "/api/warehouses", + method: "post", + data, + }); + } + + async updateWarehouse(id: number, data: WarehouseFormData): Promise<{ data: Warehouse }> { + return await request<{ data: Warehouse }>({ + url: `/api/warehouses/${id}`, + method: "put", + data, + }); + } + + async deleteWarehouse(id: number): Promise<{ message: string }> { + return await request<{ message: string }>({ + url: `/api/warehouses/${id}`, + method: "delete", + }); + } +} + +export default WarehouseService; diff --git a/thanasoft-front/src/stores/stockStore.ts b/thanasoft-front/src/stores/stockStore.ts new file mode 100644 index 0000000..c1ce519 --- /dev/null +++ b/thanasoft-front/src/stores/stockStore.ts @@ -0,0 +1,80 @@ +import { defineStore } from "pinia"; +import StockService, { StockItem, StockMove } from "@/services/stock"; + +const stockService = new StockService(); + +export const useStockStore = defineStore("stock", { + state: () => ({ + stockItems: [] as StockItem[], + stockMoves: [] as StockMove[], + loading: false, + error: null as string | null, + }), + + actions: { + // Stock Items + async fetchStockItems() { + this.loading = true; + this.error = null; + try { + const response = await stockService.getAllStockItems(); + this.stockItems = response.data; + return this.stockItems; + } catch (error: any) { + this.error = error.message || "Erreur lors du chargement des stocks"; + throw error; + } finally { + this.loading = false; + } + }, + + async updateStockItem(id: number, data: any) { + this.loading = true; + this.error = null; + try { + const response = await stockService.updateStockItem(id, data); + const index = this.stockItems.findIndex((item) => item.id === id); + if (index !== -1) { + this.stockItems[index] = response.data; + } + return response.data; + } catch (error: any) { + this.error = error.message || "Erreur lors de la mise à jour du stock"; + throw error; + } finally { + this.loading = false; + } + }, + + // Stock Moves + async fetchStockMoves() { + this.loading = true; + this.error = null; + try { + const response = await stockService.getAllStockMoves(); + this.stockMoves = response.data; + return this.stockMoves; + } catch (error: any) { + this.error = error.message || "Erreur lors du chargement des mouvements de stock"; + throw error; + } finally { + this.loading = false; + } + }, + + async createStockMove(data: any) { + this.loading = true; + this.error = null; + try { + const response = await stockService.createStockMove(data); + this.stockMoves.unshift(response.data); + return response.data; + } catch (error: any) { + this.error = error.message || "Erreur lors de l'enregistrement du mouvement de stock"; + throw error; + } finally { + this.loading = false; + } + }, + }, +}); diff --git a/thanasoft-front/src/stores/warehouseStore.ts b/thanasoft-front/src/stores/warehouseStore.ts new file mode 100644 index 0000000..822d026 --- /dev/null +++ b/thanasoft-front/src/stores/warehouseStore.ts @@ -0,0 +1,98 @@ +import { defineStore } from "pinia"; +import WarehouseService, { Warehouse, WarehouseFormData } from "@/services/warehouse"; + +const warehouseService = new WarehouseService(); + +export const useWarehouseStore = defineStore("warehouse", { + state: () => ({ + warehouses: [] as Warehouse[], + currentWarehouse: null as Warehouse | null, + loading: false, + error: null as string | null, + }), + + actions: { + async fetchWarehouses() { + this.loading = true; + this.error = null; + try { + const response = await warehouseService.getAllWarehouses(); + this.warehouses = response.data; + return this.warehouses; + } catch (error: any) { + this.error = error.message || "Erreur lors du chargement des entrepôts"; + throw error; + } finally { + this.loading = false; + } + }, + + async fetchWarehouse(id: number) { + this.loading = true; + this.error = null; + try { + const response = await warehouseService.getWarehouse(id); + this.currentWarehouse = response.data; + return this.currentWarehouse; + } catch (error: any) { + this.error = error.message || "Erreur lors du chargement de l'entrepôt"; + throw error; + } finally { + this.loading = false; + } + }, + + async createWarehouse(data: WarehouseFormData) { + this.loading = true; + this.error = null; + try { + const response = await warehouseService.createWarehouse(data); + this.warehouses.push(response.data); + return response.data; + } catch (error: any) { + this.error = error.message || "Erreur lors de la création de l'entrepôt"; + throw error; + } finally { + this.loading = false; + } + }, + + async updateWarehouse(id: number, data: WarehouseFormData) { + this.loading = true; + this.error = null; + try { + const response = await warehouseService.updateWarehouse(id, data); + const index = this.warehouses.findIndex((w) => w.id === id); + if (index !== -1) { + this.warehouses[index] = response.data; + } + if (this.currentWarehouse?.id === id) { + this.currentWarehouse = response.data; + } + return response.data; + } catch (error: any) { + this.error = error.message || "Erreur lors de la mise à jour de l'entrepôt"; + throw error; + } finally { + this.loading = false; + } + }, + + async deleteWarehouse(id: number) { + this.loading = true; + this.error = null; + try { + await warehouseService.deleteWarehouse(id); + this.warehouses = this.warehouses.filter((w) => w.id !== id); + if (this.currentWarehouse?.id === id) { + this.currentWarehouse = null; + } + } catch (error: any) { + this.error = error.message || "Erreur lors de la suppression de l'entrepôt"; + throw error; + } finally { + this.loading = false; + } + }, + }, +}); diff --git a/thanasoft-front/src/views/pages/Stock/EditWarehouse.vue b/thanasoft-front/src/views/pages/Stock/EditWarehouse.vue new file mode 100644 index 0000000..21d0504 --- /dev/null +++ b/thanasoft-front/src/views/pages/Stock/EditWarehouse.vue @@ -0,0 +1,12 @@ + + + diff --git a/thanasoft-front/src/views/pages/Stock/NewWarehouse.vue b/thanasoft-front/src/views/pages/Stock/NewWarehouse.vue new file mode 100644 index 0000000..9c4a340 --- /dev/null +++ b/thanasoft-front/src/views/pages/Stock/NewWarehouse.vue @@ -0,0 +1,7 @@ + + + diff --git a/thanasoft-front/src/views/pages/Stock/WarehouseDetail.vue b/thanasoft-front/src/views/pages/Stock/WarehouseDetail.vue new file mode 100644 index 0000000..711397f --- /dev/null +++ b/thanasoft-front/src/views/pages/Stock/WarehouseDetail.vue @@ -0,0 +1,12 @@ + + + diff --git a/thanasoft-front/src/views/pages/Stock/WarehouseList.vue b/thanasoft-front/src/views/pages/Stock/WarehouseList.vue new file mode 100644 index 0000000..5c3865c --- /dev/null +++ b/thanasoft-front/src/views/pages/Stock/WarehouseList.vue @@ -0,0 +1,7 @@ + + +