Feature: Warehouse et aussi les API bon de reception et Stock, mouvement stock, Warehouse stock
This commit is contained in:
parent
4af8ea2c60
commit
d8927580e7
134
thanasoft-back/app/Http/Controllers/Api/StockItemController.php
Normal file
134
thanasoft-back/app/Http/Controllers/Api/StockItemController.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreStockItemRequest;
|
||||||
|
use App\Http\Requests\UpdateStockItemRequest;
|
||||||
|
use App\Http\Resources\StockItemResource;
|
||||||
|
use App\Repositories\StockItemRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class StockItemController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly StockItemRepositoryInterface $stockItemRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of stock items.
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$items = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreStockMoveRequest;
|
||||||
|
use App\Http\Resources\StockMoveResource;
|
||||||
|
use App\Repositories\StockMoveRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class StockMoveController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly StockMoveRepositoryInterface $stockMoveRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of stock moves.
|
||||||
|
*/
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$moves = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
thanasoft-back/app/Http/Controllers/Api/WarehouseController.php
Normal file
134
thanasoft-back/app/Http/Controllers/Api/WarehouseController.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreWarehouseRequest;
|
||||||
|
use App\Http\Requests\UpdateWarehouseRequest;
|
||||||
|
use App\Http\Resources\WarehouseResource;
|
||||||
|
use App\Repositories\WarehouseRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class WarehouseController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WarehouseRepositoryInterface $warehouseRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of warehouses.
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$warehouses = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
thanasoft-back/app/Http/Requests/StoreStockItemRequest.php
Normal file
48
thanasoft-back/app/Http/Requests/StoreStockItemRequest.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreStockItemRequest 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 [
|
||||||
|
'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<string, string>
|
||||||
|
*/
|
||||||
|
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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
58
thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php
Normal file
58
thanasoft-back/app/Http/Requests/StoreStockMoveRequest.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreStockMoveRequest 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 [
|
||||||
|
'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<string, string>
|
||||||
|
*/
|
||||||
|
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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
52
thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php
Normal file
52
thanasoft-back/app/Http/Requests/StoreWarehouseRequest.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreWarehouseRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max: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<string, string>
|
||||||
|
*/
|
||||||
|
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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
42
thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php
Normal file
42
thanasoft-back/app/Http/Requests/UpdateStockItemRequest.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateStockItemRequest 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 [
|
||||||
|
'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<string, string>
|
||||||
|
*/
|
||||||
|
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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
52
thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php
Normal file
52
thanasoft-back/app/Http/Requests/UpdateWarehouseRequest.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateWarehouseRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'sometimes|required|string|max: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<string, string>
|
||||||
|
*/
|
||||||
|
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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class ProductPackagingResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
thanasoft-back/app/Http/Resources/StockItemResource.php
Normal file
30
thanasoft-back/app/Http/Resources/StockItemResource.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Http\Resources\Product\ProductResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class StockItemResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
38
thanasoft-back/app/Http/Resources/StockMoveResource.php
Normal file
38
thanasoft-back/app/Http/Resources/StockMoveResource.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Http\Resources\Product\ProductResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class StockMoveResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
thanasoft-back/app/Http/Resources/WarehouseResource.php
Normal file
29
thanasoft-back/app/Http/Resources/WarehouseResource.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class WarehouseResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -99,6 +99,30 @@ class Product extends Model
|
|||||||
return null;
|
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
|
* Boot the model
|
||||||
*/
|
*/
|
||||||
|
|||||||
30
thanasoft-back/app/Models/Stock/ProductPackaging.php
Normal file
30
thanasoft-back/app/Models/Stock/ProductPackaging.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Stock;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ProductPackaging extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $fillable = [
|
||||||
|
'product_id',
|
||||||
|
'name',
|
||||||
|
'qty_base',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'qty_base' => 'decimal:3',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the product that owns the packaging.
|
||||||
|
*/
|
||||||
|
public function product(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
thanasoft-back/app/Models/StockItem.php
Normal file
39
thanasoft-back/app/Models/StockItem.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class StockItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $fillable = [
|
||||||
|
'product_id',
|
||||||
|
'warehouse_id',
|
||||||
|
'qty_on_hand_base',
|
||||||
|
'safety_stock_base',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'qty_on_hand_base' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
thanasoft-back/app/Models/StockMove.php
Normal file
65
thanasoft-back/app/Models/StockMove.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Stock\ProductPackaging;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class StockMove extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $fillable = [
|
||||||
|
'product_id',
|
||||||
|
'from_warehouse_id',
|
||||||
|
'to_warehouse_id',
|
||||||
|
'packaging_id',
|
||||||
|
'packages_qty',
|
||||||
|
'units_qty',
|
||||||
|
'qty_base',
|
||||||
|
'move_type',
|
||||||
|
'ref_type',
|
||||||
|
'ref_id',
|
||||||
|
'moved_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'packages_qty' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
44
thanasoft-back/app/Models/Warehouse.php
Normal file
44
thanasoft-back/app/Models/Warehouse.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Warehouse extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'address_line1',
|
||||||
|
'address_line2',
|
||||||
|
'postal_code',
|
||||||
|
'city',
|
||||||
|
'country_code',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stock items for the warehouse.
|
||||||
|
*/
|
||||||
|
public function stockItems(): HasMany
|
||||||
|
{
|
||||||
|
return $this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,7 +24,10 @@ class RepositoryServiceProvider extends ServiceProvider
|
|||||||
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
||||||
$this->app->bind(FileRepositoryInterface::class, FileRepository::class);
|
$this->app->bind(FileRepositoryInterface::class, FileRepository::class);
|
||||||
$this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\Stock\ProductPackaging;
|
||||||
|
|
||||||
|
class ProductPackagingRepository extends BaseRepository implements ProductPackagingRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(ProductPackaging $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface ProductPackagingRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
15
thanasoft-back/app/Repositories/StockItemRepository.php
Normal file
15
thanasoft-back/app/Repositories/StockItemRepository.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\StockItem;
|
||||||
|
|
||||||
|
class StockItemRepository extends BaseRepository implements StockItemRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(StockItem $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface StockItemRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
15
thanasoft-back/app/Repositories/StockMoveRepository.php
Normal file
15
thanasoft-back/app/Repositories/StockMoveRepository.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\StockMove;
|
||||||
|
|
||||||
|
class StockMoveRepository extends BaseRepository implements StockMoveRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(StockMove $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface StockMoveRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
15
thanasoft-back/app/Repositories/WarehouseRepository.php
Normal file
15
thanasoft-back/app/Repositories/WarehouseRepository.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
|
||||||
|
class WarehouseRepository extends BaseRepository implements WarehouseRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(Warehouse $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface WarehouseRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\RepositoryServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Stock\ProductPackaging;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Stock\ProductPackaging>
|
||||||
|
*/
|
||||||
|
class ProductPackagingFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ProductPackaging::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'product_id' => Product::factory(),
|
||||||
|
'name' => $this->faker->word(),
|
||||||
|
'qty_base' => $this->faker->randomFloat(3, 1, 100),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
thanasoft-back/database/factories/StockItemFactory.php
Normal file
31
thanasoft-back/database/factories/StockItemFactory.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\StockItem;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StockItem>
|
||||||
|
*/
|
||||||
|
class StockItemFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = StockItem::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
thanasoft-back/database/factories/StockMoveFactory.php
Normal file
33
thanasoft-back/database/factories/StockMoveFactory.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\StockMove;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StockMove>
|
||||||
|
*/
|
||||||
|
class StockMoveFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = StockMove::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
thanasoft-back/database/factories/WarehouseFactory.php
Normal file
31
thanasoft-back/database/factories/WarehouseFactory.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Warehouse>
|
||||||
|
*/
|
||||||
|
class WarehouseFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Warehouse::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?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('warehouses', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?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_packagings', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<?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('stock_items', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?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('stock_moves', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -98,6 +98,13 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
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']);
|
||||||
|
|
||||||
|
// 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
|
// Product Category management
|
||||||
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
||||||
Route::get('/product-categories/active', [ProductCategoryController::class, 'active']);
|
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::apiResource('practitioner-documents', PractitionerDocumentController::class);
|
||||||
Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']);
|
Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']);
|
||||||
|
|
||||||
// Deceased Routes
|
// Deceased Routes
|
||||||
Route::prefix('deceased')->group(function () {
|
Route::prefix('deceased')->group(function () {
|
||||||
Route::get('/searchBy', [DeceasedController::class, 'searchBy']);
|
Route::get('/searchBy', [DeceasedController::class, 'searchBy']);
|
||||||
Route::get('/', [DeceasedController::class, 'index']);
|
Route::get('/', [DeceasedController::class, 'index']);
|
||||||
|
|||||||
65
thanasoft-back/tests/Feature/StockMoveApiTest.php
Normal file
65
thanasoft-back/tests/Feature/StockMoveApiTest.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use App\Models\StockMove;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class StockMoveApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Sanctum::actingAs(User::factory()->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.']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
thanasoft-back/tests/Feature/WarehouseApiTest.php
Normal file
89
thanasoft-back/tests/Feature/WarehouseApiTest.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class WarehouseApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Sanctum::actingAs(User::factory()->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.']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="loading" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="text-center py-5 text-danger">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="warehouse" class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header pb-0 p-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 d-flex align-items-center">
|
||||||
|
<h6 class="mb-0">Détails de l'entrepôt: {{ warehouse.name }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<soft-button color="info" variant="outline" size="sm" @click="handleEdit">
|
||||||
|
<i class="fas fa-user-edit me-2"></i> Modifier
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<warehouse-detail-info :warehouse="warehouse" />
|
||||||
|
<hr class="horizontal dark my-4" />
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<soft-button color="secondary" variant="gradient" @click="handleBack">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i> Retour à la liste
|
||||||
|
</soft-button>
|
||||||
|
<soft-button color="danger" variant="outline" @click="handleDelete">
|
||||||
|
<i class="fas fa-trash me-2"></i> Supprimer
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useWarehouseStore } from "@/stores/warehouseStore";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import WarehouseDetailInfo from "@/components/molecules/Stock/WarehouseDetailInfo.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
warehouseId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const warehouseStore = useWarehouseStore();
|
||||||
|
const warehouse = ref(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const fetchedWarehouse = await warehouseStore.fetchWarehouse(props.warehouseId);
|
||||||
|
warehouse.value = fetchedWarehouse;
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Impossible de charger l'entrepôt.";
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
router.push(`/stock/warehouses/${props.warehouseId}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.push("/stock/warehouses");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cet entrepôt ?")) {
|
||||||
|
try {
|
||||||
|
await warehouseStore.deleteWarehouse(props.warehouseId);
|
||||||
|
router.push("/stock/warehouses");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete warehouse", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header pb-0 p-3">
|
||||||
|
<h6 class="mb-0">{{ isEdit ? 'Modifier' : 'Nouvel' }} Entrepôt</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<warehouse-form
|
||||||
|
v-model="form"
|
||||||
|
:submitting="submitting"
|
||||||
|
:error="error"
|
||||||
|
:submit-label="isEdit ? 'Mettre à jour' : 'Créer'"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, defineProps, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useWarehouseStore } from "@/stores/warehouseStore";
|
||||||
|
import WarehouseForm from "@/components/molecules/Stock/WarehouseForm.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
warehouseId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const warehouseStore = useWarehouseStore();
|
||||||
|
const isEdit = computed(() => !!props.warehouseId);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
address_line1: "",
|
||||||
|
address_line2: "",
|
||||||
|
postal_code: "",
|
||||||
|
city: "",
|
||||||
|
country_code: "FR",
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitting = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (isEdit.value) {
|
||||||
|
try {
|
||||||
|
const warehouse = await warehouseStore.fetchWarehouse(props.warehouseId);
|
||||||
|
if (warehouse) {
|
||||||
|
form.value = {
|
||||||
|
name: warehouse.name,
|
||||||
|
address_line1: warehouse.address_line1 || "",
|
||||||
|
address_line2: warehouse.address_line2 || "",
|
||||||
|
postal_code: warehouse.postal_code || "",
|
||||||
|
city: warehouse.city || "",
|
||||||
|
country_code: warehouse.country_code || "FR",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Impossible de charger les données de l'entrepôt.";
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
submitting.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await warehouseStore.updateWarehouse(props.warehouseId, form.value);
|
||||||
|
} else {
|
||||||
|
await warehouseStore.createWarehouse(form.value);
|
||||||
|
}
|
||||||
|
router.push("/stock/warehouses");
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e.response?.data?.message || e.message || "Une erreur est survenue lors de l'enregistrement.";
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">Entrepôts</h5>
|
||||||
|
<p class="text-sm mb-0">Gestion des lieux de stockage et dépôts.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-sm-0 mt-3">
|
||||||
|
<soft-button color="info" variant="gradient" @click="handleCreate">
|
||||||
|
<i class="fas fa-plus me-2"></i> Nouvel Entrepôt
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<warehouse-table
|
||||||
|
:data="warehouses"
|
||||||
|
:loading="loading"
|
||||||
|
@view="handleView"
|
||||||
|
@edit="handleEdit"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import WarehouseTable from "@/components/molecules/Tables/Stock/WarehouseTable.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { useWarehouseStore } from "@/stores/warehouseStore";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const warehouseStore = useWarehouseStore();
|
||||||
|
const { warehouses, loading } = storeToRefs(warehouseStore);
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
router.push("/stock/warehouses/new");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (id) => {
|
||||||
|
router.push(`/stock/warehouses/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (id) => {
|
||||||
|
router.push(`/stock/warehouses/${id}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cet entrepôt ?")) {
|
||||||
|
try {
|
||||||
|
await warehouseStore.deleteWarehouse(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete warehouse", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
warehouseStore.fetchWarehouses();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item border-0 ps-0 pt-0 text-sm">
|
||||||
|
<strong class="text-dark">Nom de l'entrepôt:</strong> {{ warehouse.name }}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
|
<strong class="text-dark">Adresse 1:</strong> {{ warehouse.address_line1 || '-' }}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
|
<strong class="text-dark">Adresse 2:</strong> {{ warehouse.address_line2 || '-' }}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
|
<strong class="text-dark">Code Postal:</strong> {{ warehouse.postal_code || '-' }}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
|
<strong class="text-dark">Ville:</strong> {{ warehouse.city || '-' }}
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
|
<strong class="text-dark">Pays:</strong> {{ warehouse.country_code }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
warehouse: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
111
thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue
Normal file
111
thanasoft-front/src/components/molecules/Stock/WarehouseForm.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="$emit('submit')">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-3">
|
||||||
|
<label class="form-label">Nom de l'entrepôt *</label>
|
||||||
|
<input
|
||||||
|
v-model="modelValue.name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Ex: Entrepôt Principal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Adresse 1</label>
|
||||||
|
<input
|
||||||
|
v-model="modelValue.address_line1"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Numéro et rue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Adresse 2</label>
|
||||||
|
<input
|
||||||
|
v-model="modelValue.address_line2"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Appartement, lot, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Code Postal</label>
|
||||||
|
<input
|
||||||
|
v-model="modelValue.postal_code"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Ex: 75001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Ville</label>
|
||||||
|
<input
|
||||||
|
v-model="modelValue.city"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Ex: Paris"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Code Pays</label>
|
||||||
|
<input
|
||||||
|
v-model="modelValue.country_code"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Ex: FR"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="alert alert-danger text-white text-sm mt-3" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-4">
|
||||||
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="gradient"
|
||||||
|
class="me-2"
|
||||||
|
@click="$emit('cancel')"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="gradient"
|
||||||
|
:loading="submitting"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{{ submitLabel }}
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
submitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
submitLabel: {
|
||||||
|
type: String,
|
||||||
|
default: "Enregistrer",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:modelValue", "submit", "cancel"]);
|
||||||
|
</script>
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="warehouse-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Ville</th>
|
||||||
|
<th>Code Postal</th>
|
||||||
|
<th>Adresse</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="warehouse in data" :key="warehouse.id">
|
||||||
|
<!-- Name -->
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-checkbox class="me-2" />
|
||||||
|
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||||
|
{{ warehouse.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- City -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">{{ warehouse.city || '-' }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Postal Code -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">{{ warehouse.postal_code || '-' }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Address -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">
|
||||||
|
{{ warehouse.address_line1 }}
|
||||||
|
<span v-if="warehouse.address_line2" class="d-block text-secondary">{{ warehouse.address_line2 }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-secondary mb-0 px-2"
|
||||||
|
:data-id="warehouse.id"
|
||||||
|
data-action="view"
|
||||||
|
title="Voir l'entrepôt"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-info mb-0 px-2"
|
||||||
|
:data-id="warehouse.id"
|
||||||
|
data-action="edit"
|
||||||
|
title="Modifier l'entrepôt"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-edit text-xs" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-danger mb-0 px-2"
|
||||||
|
:data-id="warehouse.id"
|
||||||
|
data-action="delete"
|
||||||
|
title="Supprimer l'entrepôt"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
watch,
|
||||||
|
onUnmounted,
|
||||||
|
defineProps,
|
||||||
|
defineEmits,
|
||||||
|
} from "vue";
|
||||||
|
import { DataTable } from "simple-datatables";
|
||||||
|
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "edit", "delete"]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
|
const initializeDataTable = () => {
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTableEl = document.getElementById("warehouse-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
|
searchable: true,
|
||||||
|
fixedHeight: false,
|
||||||
|
perPageSelect: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.loading && props.data.length > 0) {
|
||||||
|
initializeDataTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event delegation
|
||||||
|
const table = document.getElementById("warehouse-list");
|
||||||
|
if (table) {
|
||||||
|
table.addEventListener("click", (event) => {
|
||||||
|
const btn = event.target.closest("button");
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const id = btn.getAttribute("data-id");
|
||||||
|
const action = btn.getAttribute("data-action");
|
||||||
|
|
||||||
|
if (id && action) {
|
||||||
|
if (action === "view") {
|
||||||
|
emit("view", parseInt(id));
|
||||||
|
} else if (action === "edit") {
|
||||||
|
emit("edit", parseInt(id));
|
||||||
|
} else if (action === "delete") {
|
||||||
|
emit("delete", parseInt(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!props.loading) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -319,6 +319,12 @@ export default {
|
|||||||
miniIcon: "S",
|
miniIcon: "S",
|
||||||
text: "Stock",
|
text: "Stock",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "warehouses",
|
||||||
|
route: { name: "Entrepôts" },
|
||||||
|
miniIcon: "E",
|
||||||
|
text: "Entrepôts",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -614,6 +614,27 @@ const routes = [
|
|||||||
name: "Product details",
|
name: "Product details",
|
||||||
component: () => import("@/views/pages/Stock/ProductDetails.vue"),
|
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
|
// Employés
|
||||||
{
|
{
|
||||||
path: "/employes",
|
path: "/employes",
|
||||||
|
|||||||
51
thanasoft-front/src/services/productPackaging.ts
Normal file
51
thanasoft-front/src/services/productPackaging.ts
Normal file
@ -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;
|
||||||
91
thanasoft-front/src/services/stock.ts
Normal file
91
thanasoft-front/src/services/stock.ts
Normal file
@ -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;
|
||||||
63
thanasoft-front/src/services/warehouse.ts
Normal file
63
thanasoft-front/src/services/warehouse.ts
Normal file
@ -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;
|
||||||
80
thanasoft-front/src/stores/stockStore.ts
Normal file
80
thanasoft-front/src/stores/stockStore.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
98
thanasoft-front/src/stores/warehouseStore.ts
Normal file
98
thanasoft-front/src/stores/warehouseStore.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
12
thanasoft-front/src/views/pages/Stock/EditWarehouse.vue
Normal file
12
thanasoft-front/src/views/pages/Stock/EditWarehouse.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<warehouse-form-presentation :warehouse-id="warehouseId" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import WarehouseFormPresentation from "@/components/Organism/Stock/WarehouseFormPresentation.vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const warehouseId = computed(() => route.params.id);
|
||||||
|
</script>
|
||||||
7
thanasoft-front/src/views/pages/Stock/NewWarehouse.vue
Normal file
7
thanasoft-front/src/views/pages/Stock/NewWarehouse.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<warehouse-form-presentation />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import WarehouseFormPresentation from "@/components/Organism/Stock/WarehouseFormPresentation.vue";
|
||||||
|
</script>
|
||||||
12
thanasoft-front/src/views/pages/Stock/WarehouseDetail.vue
Normal file
12
thanasoft-front/src/views/pages/Stock/WarehouseDetail.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<warehouse-detail-presentation :warehouse-id="warehouseId" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import WarehouseDetailPresentation from "@/components/Organism/Stock/WarehouseDetailPresentation.vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const warehouseId = computed(() => route.params.id);
|
||||||
|
</script>
|
||||||
7
thanasoft-front/src/views/pages/Stock/WarehouseList.vue
Normal file
7
thanasoft-front/src/views/pages/Stock/WarehouseList.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<warehouse-list-presentation />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import WarehouseListPresentation from "@/components/Organism/Stock/WarehouseListPresentation.vue";
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user