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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
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(FileRepositoryInterface::class, FileRepository::class);
|
||||
$this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class);
|
||||
|
||||
$this->app->bind(\App\Repositories\WarehouseRepositoryInterface::class, \App\Repositories\WarehouseRepository::class);
|
||||
$this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class);
|
||||
$this->app->bind(\App\Repositories\StockMoveRepositoryInterface::class, \App\Repositories\StockMoveRepository::class);
|
||||
$this->app->bind(\App\Repositories\ProductPackagingRepositoryInterface::class, \App\Repositories\ProductPackagingRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 [
|
||||
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::patch('/products/{id}/stock', [ProductController::class, 'updateStock']);
|
||||
|
||||
// Warehouse management
|
||||
Route::apiResource('warehouses', \App\Http\Controllers\Api\WarehouseController::class);
|
||||
|
||||
// Stock management
|
||||
Route::apiResource('stock-items', \App\Http\Controllers\Api\StockItemController::class);
|
||||
Route::apiResource('stock-moves', \App\Http\Controllers\Api\StockMoveController::class);
|
||||
|
||||
// Product Category management
|
||||
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
||||
Route::get('/product-categories/active', [ProductCategoryController::class, 'active']);
|
||||
@ -124,7 +131,7 @@ Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::apiResource('practitioner-documents', PractitionerDocumentController::class);
|
||||
Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']);
|
||||
|
||||
// Deceased Routes
|
||||
// Deceased Routes
|
||||
Route::prefix('deceased')->group(function () {
|
||||
Route::get('/searchBy', [DeceasedController::class, 'searchBy']);
|
||||
Route::get('/', [DeceasedController::class, 'index']);
|
||||
|
||||
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",
|
||||
text: "Stock",
|
||||
},
|
||||
{
|
||||
id: "warehouses",
|
||||
route: { name: "Entrepôts" },
|
||||
miniIcon: "E",
|
||||
text: "Entrepôts",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -614,6 +614,27 @@ const routes = [
|
||||
name: "Product details",
|
||||
component: () => import("@/views/pages/Stock/ProductDetails.vue"),
|
||||
},
|
||||
// Warehouses
|
||||
{
|
||||
path: "/stock/warehouses",
|
||||
name: "Entrepôts",
|
||||
component: () => import("@/views/pages/Stock/WarehouseList.vue"),
|
||||
},
|
||||
{
|
||||
path: "/stock/warehouses/new",
|
||||
name: "Nouvel Entrepôt",
|
||||
component: () => import("@/views/pages/Stock/NewWarehouse.vue"),
|
||||
},
|
||||
{
|
||||
path: "/stock/warehouses/:id",
|
||||
name: "Détails Entrepôt",
|
||||
component: () => import("@/views/pages/Stock/WarehouseDetail.vue"),
|
||||
},
|
||||
{
|
||||
path: "/stock/warehouses/:id/edit",
|
||||
name: "Modifier Entrepôt",
|
||||
component: () => import("@/views/pages/Stock/EditWarehouse.vue"),
|
||||
},
|
||||
// Employés
|
||||
{
|
||||
path: "/employes",
|
||||
|
||||
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