Stock et achats: amélioration des réceptions, entrepôts et commandes fournisseurs
This commit is contained in:
parent
094c7a0980
commit
a9a2429b67
@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Requests\StoreGoodsReceiptRequest;
|
use App\Http\Requests\StoreGoodsReceiptRequest;
|
||||||
use App\Http\Requests\UpdateGoodsReceiptRequest;
|
use App\Http\Requests\UpdateGoodsReceiptRequest;
|
||||||
use App\Http\Resources\GoodsReceiptResource;
|
use App\Http\Resources\GoodsReceiptResource;
|
||||||
|
use App\Models\PurchaseOrder;
|
||||||
use App\Repositories\GoodsReceiptRepositoryInterface;
|
use App\Repositories\GoodsReceiptRepositoryInterface;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@ -45,7 +46,32 @@ class GoodsReceiptController extends Controller
|
|||||||
public function store(StoreGoodsReceiptRequest $request): JsonResponse
|
public function store(StoreGoodsReceiptRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$goodsReceipt = $this->goodsReceiptRepository->create($request->validated());
|
$payload = $request->validated();
|
||||||
|
|
||||||
|
if (empty($payload['lines']) && !empty($payload['purchase_order_id'])) {
|
||||||
|
$purchaseOrder = PurchaseOrder::query()
|
||||||
|
->with('lines')
|
||||||
|
->find($payload['purchase_order_id']);
|
||||||
|
|
||||||
|
if ($purchaseOrder) {
|
||||||
|
$payload['lines'] = $purchaseOrder->lines
|
||||||
|
->filter(fn($line) => !empty($line->product_id))
|
||||||
|
->map(fn($line) => [
|
||||||
|
'product_id' => (int) $line->product_id,
|
||||||
|
'packaging_id' => null,
|
||||||
|
'packages_qty_received' => null,
|
||||||
|
'units_qty_received' => (float) $line->quantity,
|
||||||
|
'qty_received_base' => (float) $line->quantity,
|
||||||
|
'unit_price' => (float) $line->unit_price,
|
||||||
|
'unit_price_per_package' => null,
|
||||||
|
'tva_rate_id' => null,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$goodsReceipt = $this->goodsReceiptRepository->create($payload);
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => new GoodsReceiptResource($goodsReceipt),
|
'data' => new GoodsReceiptResource($goodsReceipt),
|
||||||
'message' => 'Réception de marchandise créée avec succès.',
|
'message' => 'Réception de marchandise créée avec succès.',
|
||||||
|
|||||||
@ -8,6 +8,9 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Requests\StorePurchaseOrderRequest;
|
use App\Http\Requests\StorePurchaseOrderRequest;
|
||||||
use App\Http\Requests\UpdatePurchaseOrderRequest;
|
use App\Http\Requests\UpdatePurchaseOrderRequest;
|
||||||
use App\Http\Resources\Fournisseur\PurchaseOrderResource;
|
use App\Http\Resources\Fournisseur\PurchaseOrderResource;
|
||||||
|
use App\Models\GoodsReceipt;
|
||||||
|
use App\Models\Warehouse;
|
||||||
|
use App\Repositories\GoodsReceiptRepositoryInterface;
|
||||||
use App\Repositories\PurchaseOrderRepositoryInterface;
|
use App\Repositories\PurchaseOrderRepositoryInterface;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
@ -16,7 +19,8 @@ use Illuminate\Support\Facades\Log;
|
|||||||
class PurchaseOrderController extends Controller
|
class PurchaseOrderController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected PurchaseOrderRepositoryInterface $purchaseOrderRepository
|
protected PurchaseOrderRepositoryInterface $purchaseOrderRepository,
|
||||||
|
protected GoodsReceiptRepositoryInterface $goodsReceiptRepository
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +102,9 @@ class PurchaseOrderController extends Controller
|
|||||||
public function update(UpdatePurchaseOrderRequest $request, string $id): PurchaseOrderResource|JsonResponse
|
public function update(UpdatePurchaseOrderRequest $request, string $id): PurchaseOrderResource|JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$existingPurchaseOrder = $this->purchaseOrderRepository->find($id);
|
||||||
|
$previousStatus = $existingPurchaseOrder?->status;
|
||||||
|
|
||||||
$updated = $this->purchaseOrderRepository->update($id, $request->validated());
|
$updated = $this->purchaseOrderRepository->update($id, $request->validated());
|
||||||
|
|
||||||
if (!$updated) {
|
if (!$updated) {
|
||||||
@ -107,6 +114,16 @@ class PurchaseOrderController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$purchaseOrder = $this->purchaseOrderRepository->find($id);
|
$purchaseOrder = $this->purchaseOrderRepository->find($id);
|
||||||
|
|
||||||
|
// On validation/delivery (status => confirmee|livree), create a draft goods receipt automatically.
|
||||||
|
if (
|
||||||
|
$purchaseOrder
|
||||||
|
&& in_array($purchaseOrder->status, ['confirmee', 'livree'], true)
|
||||||
|
&& !in_array($previousStatus, ['confirmee', 'livree'], true)
|
||||||
|
) {
|
||||||
|
$this->createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder);
|
||||||
|
}
|
||||||
|
|
||||||
return new PurchaseOrderResource($purchaseOrder);
|
return new PurchaseOrderResource($purchaseOrder);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error updating purchase order: ' . $e->getMessage(), [
|
Log::error('Error updating purchase order: ' . $e->getMessage(), [
|
||||||
@ -123,6 +140,53 @@ class PurchaseOrderController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a draft goods receipt when a purchase order is validated.
|
||||||
|
*/
|
||||||
|
protected function createGoodsReceiptFromValidatedPurchaseOrder($purchaseOrder): void
|
||||||
|
{
|
||||||
|
$alreadyExists = GoodsReceipt::query()
|
||||||
|
->where('purchase_order_id', $purchaseOrder->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($alreadyExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$warehouseId = Warehouse::query()->value('id');
|
||||||
|
if (!$warehouseId) {
|
||||||
|
throw new \RuntimeException('Aucun entrepôt disponible pour créer la réception de marchandise.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$receiptNumber = 'GR-' . now()->format('Ym') . '-' . str_pad((string) $purchaseOrder->id, 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$lines = collect($purchaseOrder->lines ?? [])
|
||||||
|
->filter(fn($line) => !empty($line->product_id))
|
||||||
|
->map(function ($line) {
|
||||||
|
return [
|
||||||
|
'product_id' => (int) $line->product_id,
|
||||||
|
'packaging_id' => null,
|
||||||
|
'packages_qty_received' => null,
|
||||||
|
'units_qty_received' => (float) $line->quantity,
|
||||||
|
'qty_received_base' => (float) $line->quantity,
|
||||||
|
'unit_price' => (float) $line->unit_price,
|
||||||
|
'unit_price_per_package' => null,
|
||||||
|
'tva_rate_id' => null,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->goodsReceiptRepository->create([
|
||||||
|
'purchase_order_id' => $purchaseOrder->id,
|
||||||
|
'warehouse_id' => (int) $warehouseId,
|
||||||
|
'receipt_number' => $receiptNumber,
|
||||||
|
'receipt_date' => now()->toDateString(),
|
||||||
|
'status' => 'draft',
|
||||||
|
'lines' => $lines,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the specified purchase order.
|
* Remove the specified purchase order.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources;
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Http\Resources\Fournisseur\PurchaseOrderResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
|||||||
@ -7,178 +7,208 @@
|
|||||||
<div v-else-if="error" class="text-center py-5 text-danger">
|
<div v-else-if="error" class="text-center py-5 text-danger">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="commande" class="commande-detail">
|
<div v-else-if="commande">
|
||||||
<!-- Header Section -->
|
<div class="odoo-toolbar">
|
||||||
<div class="form-section">
|
<div class="statusbar-wrapper">
|
||||||
<div class="section-title">
|
<button
|
||||||
<i class="fas fa-file-invoice"></i>
|
v-for="status in availableStatuses"
|
||||||
Informations générales
|
:key="status"
|
||||||
</div>
|
type="button"
|
||||||
|
class="status-step"
|
||||||
<!-- Row 1: Commande Number, Date, Status -->
|
:class="{ active: status === commande.status }"
|
||||||
<div class="row g-3 mb-3">
|
:disabled="true"
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Numéro commande</label>
|
|
||||||
<div class="info-value">{{ commande.number }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Date commande</label>
|
|
||||||
<div class="info-value">{{ formatDate(commande.date) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Statut</label>
|
|
||||||
<div class="status-badge" :class="getStatusClass(commande.status)">
|
|
||||||
<i :class="getStatusIcon(commande.status)"></i>
|
|
||||||
{{ getStatusLabel(commande.status) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Row 2: Fournisseur, Contact -->
|
|
||||||
<div class="row g-3 mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Fournisseur</label>
|
|
||||||
<div class="info-value">{{ commande.supplierName }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Contact</label>
|
|
||||||
<div class="info-value">{{ commande.supplierContact }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Row 3: Adresse livraison -->
|
|
||||||
<div class="mb-0">
|
|
||||||
<label class="form-label">Adresse livraison</label>
|
|
||||||
<div class="info-value">{{ commande.deliveryAddress || 'Non spécifiée' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Articles Section -->
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<i class="fas fa-boxes"></i>
|
|
||||||
Articles commandés
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lines-container">
|
|
||||||
<div
|
|
||||||
v-for="line in commande.lines"
|
|
||||||
:key="line.id"
|
|
||||||
class="line-item"
|
|
||||||
>
|
>
|
||||||
<div class="row g-2 align-items-center">
|
<i :class="getStatusIcon(status)"></i>
|
||||||
<div class="col-md-5">
|
<span>{{ getStatusLabel(status) }}</span>
|
||||||
<label class="form-label text-xs">Désignation</label>
|
</button>
|
||||||
<div class="line-designation">{{ line.designation }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label text-xs">Quantité</label>
|
|
||||||
<div class="line-quantity">{{ line.quantity }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label text-xs">Prix HT</label>
|
|
||||||
<div class="line-price">{{ formatCurrency(line.price_ht) }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 d-flex flex-column align-items-end">
|
|
||||||
<label class="form-label text-xs">Total HT</label>
|
|
||||||
<span class="line-total">
|
|
||||||
{{ formatCurrency(line.total_ht) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Totals Section -->
|
|
||||||
<div class="totals-section">
|
|
||||||
<div class="totals-content">
|
|
||||||
<div class="total-row">
|
|
||||||
<span class="total-label">Total HT</span>
|
|
||||||
<span class="total-value">{{ formatCurrency(commande.total_ht) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="total-row">
|
|
||||||
<span class="total-label">TVA (20%)</span>
|
|
||||||
<span class="total-value">{{ formatCurrency(commande.total_tva) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="total-row total-final">
|
|
||||||
<span class="total-label">Total TTC</span>
|
|
||||||
<span class="total-amount">{{ formatCurrency(commande.total_ttc) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Info Section -->
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="section-title">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
Informations supplémentaires
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Adresse fournisseur</label>
|
|
||||||
<div class="info-value">{{ commande.supplierAddress }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Date de création</label>
|
|
||||||
<div class="info-value">{{ formatDate(commande.date) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="commande.notes" class="mt-3">
|
<div class="toolbar-right">
|
||||||
<label class="form-label">Notes</label>
|
|
||||||
<div class="info-value notes-content">{{ commande.notes }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="action-buttons">
|
|
||||||
<div class="position-relative d-inline-block">
|
|
||||||
<soft-button
|
<soft-button
|
||||||
color="secondary"
|
color="success"
|
||||||
variant="gradient"
|
variant="outline"
|
||||||
@click="dropdownOpen = !dropdownOpen"
|
class="btn-toolbar btn-sm"
|
||||||
class="btn-status"
|
:disabled="!primaryNextStatus || isUpdatingStatus"
|
||||||
|
@click="handlePrimaryAction"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exchange-alt me-2"></i>
|
<i class="fas fa-check me-2"></i>
|
||||||
Changer le statut
|
{{ primaryActionLabel }}
|
||||||
<i class="fas fa-chevron-down ms-2"></i>
|
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<ul
|
|
||||||
v-if="dropdownOpen"
|
<soft-button
|
||||||
class="dropdown-menu show position-absolute"
|
:color="secondaryActionColor"
|
||||||
style="top: 100%; left: 0; z-index: 1000;"
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
:disabled="!secondaryActionTargetStatus || isUpdatingStatus"
|
||||||
|
@click="handleSecondaryAction"
|
||||||
>
|
>
|
||||||
<li v-for="status in availableStatuses" :key="status">
|
<i :class="`${secondaryActionIcon} me-2`"></i>
|
||||||
<a
|
{{ secondaryActionLabel }}
|
||||||
class="dropdown-item"
|
</soft-button>
|
||||||
:class="{ active: status === commande.status }"
|
|
||||||
href="javascript:;"
|
<soft-button
|
||||||
@click="changeStatus(status); dropdownOpen = false;"
|
color="dark"
|
||||||
>
|
variant="outline"
|
||||||
<i :class="getStatusIcon(status) + ' me-2'"></i>
|
class="btn-toolbar btn-sm"
|
||||||
{{ getStatusLabel(status) }}
|
@click="sendByEmail"
|
||||||
</a>
|
>
|
||||||
</li>
|
<i class="fas fa-paper-plane me-2"></i>
|
||||||
</ul>
|
Envoyer par mail
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
@click="downloadPdf"
|
||||||
|
>
|
||||||
|
<i class="fas fa-file-pdf me-2"></i>
|
||||||
|
Télécharger PDF
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="commande-detail">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-file-invoice"></i>
|
||||||
|
Informations générales
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 1: Commande Number, Date, Status -->
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Numéro commande</label>
|
||||||
|
<div class="info-value">{{ commande.number }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Date commande</label>
|
||||||
|
<div class="info-value">{{ formatDate(commande.date) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<div class="status-badge" :class="getStatusClass(commande.status)">
|
||||||
|
<i :class="getStatusIcon(commande.status)"></i>
|
||||||
|
{{ getStatusLabel(commande.status) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Fournisseur, Contact -->
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Fournisseur</label>
|
||||||
|
<div class="info-value">{{ commande.supplierName }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Contact</label>
|
||||||
|
<div class="info-value">{{ commande.supplierContact }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Adresse livraison -->
|
||||||
|
<div class="mb-0">
|
||||||
|
<label class="form-label">Adresse livraison</label>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ commande.deliveryAddress || "Non spécifiée" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<soft-button color="info" variant="outline" class="btn-pdf">
|
<!-- Articles Section -->
|
||||||
<i class="fas fa-file-pdf me-2"></i> Télécharger PDF
|
<div class="form-section">
|
||||||
</soft-button>
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-boxes"></i>
|
||||||
|
Articles commandés
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lines-container">
|
||||||
|
<div v-for="line in commande.lines" :key="line.id" class="line-item">
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label text-xs">Désignation</label>
|
||||||
|
<div class="line-designation">{{ line.designation }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label text-xs">Quantité</label>
|
||||||
|
<div class="line-quantity">{{ line.quantity }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label text-xs">Prix HT</label>
|
||||||
|
<div class="line-price">
|
||||||
|
{{ formatCurrency(line.price_ht) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 d-flex flex-column align-items-end">
|
||||||
|
<label class="form-label text-xs">Total HT</label>
|
||||||
|
<span class="line-total">
|
||||||
|
{{ formatCurrency(line.total_ht) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Totals Section -->
|
||||||
|
<div class="totals-section">
|
||||||
|
<div class="totals-content">
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="total-label">Total HT</span>
|
||||||
|
<span class="total-value">{{
|
||||||
|
formatCurrency(commande.total_ht)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="total-label">TVA (20%)</span>
|
||||||
|
<span class="total-value">{{
|
||||||
|
formatCurrency(commande.total_tva)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row total-final">
|
||||||
|
<span class="total-label">Total TTC</span>
|
||||||
|
<span class="total-amount">{{
|
||||||
|
formatCurrency(commande.total_ttc)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Info Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Informations supplémentaires
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Adresse fournisseur</label>
|
||||||
|
<div class="info-value">{{ commande.supplierAddress }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Date de création</label>
|
||||||
|
<div class="info-value">{{ formatDate(commande.date) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="commande.notes" class="mt-3">
|
||||||
|
<label class="form-label">Notes</label>
|
||||||
|
<div class="info-value notes-content">{{ commande.notes }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, onMounted } from "vue";
|
import { ref, defineProps, onMounted, computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import { PurchaseOrderService } from "@/services/purchaseOrder";
|
import { PurchaseOrderService } from "@/services/purchaseOrder";
|
||||||
import { useNotificationStore } from "@/stores/notification";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
@ -190,14 +220,71 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
const commande = ref(null);
|
const commande = ref(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
const dropdownOpen = ref(false);
|
const isUpdatingStatus = ref(false);
|
||||||
|
const statusUpdateRequestId = ref(0);
|
||||||
|
|
||||||
const availableStatuses = ["brouillon", "confirmee", "livree", "facturee", "annulee"];
|
const availableStatuses = [
|
||||||
|
"brouillon",
|
||||||
|
"confirmee",
|
||||||
|
"livree",
|
||||||
|
"facturee",
|
||||||
|
"annulee",
|
||||||
|
];
|
||||||
|
|
||||||
|
const primaryNextStatus = computed(() => {
|
||||||
|
if (!commande.value?.status) return null;
|
||||||
|
const nextMap = {
|
||||||
|
brouillon: "confirmee",
|
||||||
|
confirmee: "livree",
|
||||||
|
livree: "facturee",
|
||||||
|
};
|
||||||
|
return nextMap[commande.value.status] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryActionLabel = computed(() => {
|
||||||
|
const labels = {
|
||||||
|
confirmee: "Valider / Confirmer",
|
||||||
|
livree: "Marquer comme livrée",
|
||||||
|
facturee: "Marquer comme facturée",
|
||||||
|
};
|
||||||
|
return labels[primaryNextStatus.value] || "Aucune action";
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActionTargetStatus = computed(() => {
|
||||||
|
if (!commande.value?.status) return null;
|
||||||
|
|
||||||
|
if (["brouillon", "confirmee", "livree"].includes(commande.value.status)) {
|
||||||
|
return "annulee";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commande.value.status === "annulee") {
|
||||||
|
return "brouillon";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActionLabel = computed(() => {
|
||||||
|
if (secondaryActionTargetStatus.value === "annulee") return "Annuler";
|
||||||
|
if (secondaryActionTargetStatus.value === "brouillon") return "Remettre en brouillon";
|
||||||
|
return "Aucune action";
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActionIcon = computed(() => {
|
||||||
|
if (secondaryActionTargetStatus.value === "annulee") return "fas fa-times";
|
||||||
|
if (secondaryActionTargetStatus.value === "brouillon") return "fas fa-undo";
|
||||||
|
return "fas fa-ban";
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryActionColor = computed(() => {
|
||||||
|
if (secondaryActionTargetStatus.value === "annulee") return "danger";
|
||||||
|
if (secondaryActionTargetStatus.value === "brouillon") return "warning";
|
||||||
|
return "secondary";
|
||||||
|
});
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return "-";
|
if (!dateString) return "-";
|
||||||
@ -252,9 +339,11 @@ const fetchCommande = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const response = await PurchaseOrderService.getPurchaseOrder(props.commandeId);
|
const response = await PurchaseOrderService.getPurchaseOrder(
|
||||||
|
props.commandeId
|
||||||
|
);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// Map backend data to frontend structure
|
// Map backend data to frontend structure
|
||||||
commande.value = {
|
commande.value = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
@ -266,22 +355,25 @@ const fetchCommande = async () => {
|
|||||||
total_ttc: data.total_ttc,
|
total_ttc: data.total_ttc,
|
||||||
notes: data.notes,
|
notes: data.notes,
|
||||||
deliveryAddress: data.delivery_address,
|
deliveryAddress: data.delivery_address,
|
||||||
|
|
||||||
// Supplier mapping
|
// Supplier mapping
|
||||||
supplierName: data.fournisseur?.name || "Inconnu",
|
supplierName: data.fournisseur?.name || "Inconnu",
|
||||||
supplierAddress: data.fournisseur ?
|
supplierAddress: data.fournisseur
|
||||||
`${data.fournisseur.billing_address_line1 || ''} ${data.fournisseur.billing_city || ''}` :
|
? `${data.fournisseur.billing_address_line1 || ""} ${
|
||||||
"Non spécifiée",
|
data.fournisseur.billing_city || ""
|
||||||
supplierContact: data.fournisseur?.email || data.fournisseur?.phone || "Indisponible",
|
}`
|
||||||
|
: "Non spécifiée",
|
||||||
|
supplierContact:
|
||||||
|
data.fournisseur?.email || data.fournisseur?.phone || "Indisponible",
|
||||||
|
|
||||||
// Lines mapping: translation between backend (description, unit_price) and frontend (designation, price_ht)
|
// Lines mapping: translation between backend (description, unit_price) and frontend (designation, price_ht)
|
||||||
lines: (data.lines || []).map(line => ({
|
lines: (data.lines || []).map((line) => ({
|
||||||
id: line.id,
|
id: line.id,
|
||||||
designation: line.description,
|
designation: line.description,
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
price_ht: line.unit_price,
|
price_ht: line.unit_price,
|
||||||
total_ht: line.total_ht
|
total_ht: line.total_ht,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching commande:", err);
|
console.error("Error fetching commande:", err);
|
||||||
@ -293,20 +385,64 @@ const fetchCommande = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeStatus = async (newStatus) => {
|
const changeStatus = async (newStatus) => {
|
||||||
|
if (!commande.value || commande.value.status === newStatus) return;
|
||||||
|
|
||||||
|
const requestId = ++statusUpdateRequestId.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
isUpdatingStatus.value = true;
|
||||||
const payload = {
|
const payload = {
|
||||||
id: props.commandeId,
|
id: props.commandeId,
|
||||||
status: newStatus
|
status: newStatus,
|
||||||
};
|
};
|
||||||
await PurchaseOrderService.updatePurchaseOrder(payload);
|
|
||||||
commande.value.status = newStatus;
|
const response = await PurchaseOrderService.updatePurchaseOrder(payload);
|
||||||
notificationStore.success("Succès", `Statut mis à jour : ${getStatusLabel(newStatus)}`);
|
|
||||||
|
// Ignore stale async responses to avoid race-condition overwrites
|
||||||
|
if (requestId !== statusUpdateRequestId.value || !commande.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedStatus = response?.data?.status || newStatus;
|
||||||
|
|
||||||
|
// Do not mutate status directly in frontend; always reload from backend source.
|
||||||
|
await fetchCommande();
|
||||||
|
|
||||||
|
notificationStore.success(
|
||||||
|
"Succès",
|
||||||
|
`Statut mis à jour : ${getStatusLabel(persistedStatus)}`
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error updating status:", err);
|
console.error("Error updating status:", err);
|
||||||
notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
|
notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
|
||||||
|
} finally {
|
||||||
|
if (requestId === statusUpdateRequestId.value) {
|
||||||
|
isUpdatingStatus.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePrimaryAction = async () => {
|
||||||
|
if (!primaryNextStatus.value) return;
|
||||||
|
await changeStatus(primaryNextStatus.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondaryAction = async () => {
|
||||||
|
if (!secondaryActionTargetStatus.value) return;
|
||||||
|
await changeStatus(secondaryActionTargetStatus.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendByEmail = () => {
|
||||||
|
notificationStore.success(
|
||||||
|
"Email",
|
||||||
|
"Simulation d'envoi de commande par email effectuée."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPdf = () => {
|
||||||
|
window.print();
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCommande();
|
fetchCommande();
|
||||||
});
|
});
|
||||||
@ -318,6 +454,82 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.odoo-toolbar {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toolbar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.35rem 0.7rem !important;
|
||||||
|
font-size: 0.78rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0.38rem 0.65rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #495057;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step:not(:last-child)::after {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
right: -0.6rem;
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step:hover:not(:disabled) {
|
||||||
|
border-color: #5e72e4;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step.active {
|
||||||
|
background: #344767;
|
||||||
|
border-color: #344767;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Form Sections */
|
/* Form Sections */
|
||||||
.form-section {
|
.form-section {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -517,47 +729,38 @@ onMounted(() => {
|
|||||||
color: #212529;
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Action Buttons */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-status,
|
|
||||||
.btn-pdf {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.form-section {
|
.form-section {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals-content {
|
.totals-content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.toolbar-left .btn-toolbar {
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-status,
|
|
||||||
.btn-pdf {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusbar-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.odoo-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<commande-list-controls
|
<commande-list-controls
|
||||||
@create="openCreateModal"
|
@create="openCreateModal"
|
||||||
@filter="handleFilter"
|
@filter="handleFilter"
|
||||||
@export="handleExport"
|
@export="handleExport"
|
||||||
/>
|
/>
|
||||||
@ -75,7 +75,10 @@ const handleExport = () => {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
link.setAttribute("href", url);
|
link.setAttribute("href", url);
|
||||||
link.setAttribute("download", `commandes-export-${new Date().toISOString().split('T')[0]}.csv`);
|
link.setAttribute(
|
||||||
|
"download",
|
||||||
|
`commandes-export-${new Date().toISOString().split("T")[0]}.csv`
|
||||||
|
);
|
||||||
link.style.visibility = "hidden";
|
link.style.visibility = "hidden";
|
||||||
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
|
|||||||
@ -3,7 +3,9 @@
|
|||||||
<div class="d-sm-flex justify-content-between mb-4">
|
<div class="d-sm-flex justify-content-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="mb-0">Réceptions de Marchandises</h5>
|
<h5 class="mb-0">Réceptions de Marchandises</h5>
|
||||||
<p class="text-sm mb-0">Gestion des réceptions de marchandises en provenance des fournisseurs.</p>
|
<p class="text-sm mb-0">
|
||||||
|
Gestion des réceptions de marchandises en provenance des fournisseurs.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-sm-0 mt-3">
|
<div class="mt-sm-0 mt-3">
|
||||||
<soft-button color="info" variant="gradient" @click="handleCreate">
|
<soft-button color="info" variant="gradient" @click="handleCreate">
|
||||||
@ -17,16 +19,20 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Rechercher..."
|
placeholder="Rechercher..."
|
||||||
v-model="searchQuery"
|
|
||||||
@input="handleSearch"
|
@input="handleSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<select class="form-control" v-model="statusFilter" @change="handleFilter">
|
<select
|
||||||
|
v-model="statusFilter"
|
||||||
|
class="form-control"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
<option value="">Tous les statuts</option>
|
<option value="">Tous les statuts</option>
|
||||||
<option value="draft">Brouillon</option>
|
<option value="draft">Brouillon</option>
|
||||||
<option value="posted">Validée</option>
|
<option value="posted">Validée</option>
|
||||||
@ -37,7 +43,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<goods-receipt-table
|
<goods-receipt-table
|
||||||
:data="goodsReceipts"
|
:data="receipts"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@view="handleView"
|
@view="handleView"
|
||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@ -54,11 +60,11 @@ import { storeToRefs } from "pinia";
|
|||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import GoodsReceiptTable from "@/components/molecules/Tables/Stock/GoodsReceiptTable.vue";
|
import GoodsReceiptTable from "@/components/molecules/Tables/Stock/GoodsReceiptTable.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
import { useReceiptStore } from "@/stores/receiptStore";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const goodsReceiptStore = useGoodsReceiptStore();
|
const receiptStore = useReceiptStore();
|
||||||
const { goodsReceipts, loading } = storeToRefs(goodsReceiptStore);
|
const { receipts, loading } = storeToRefs(receiptStore);
|
||||||
|
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
const statusFilter = ref("");
|
const statusFilter = ref("");
|
||||||
@ -78,9 +84,13 @@ const handleEdit = (id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
|
if (
|
||||||
|
confirm(
|
||||||
|
"Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await goodsReceiptStore.deleteGoodsReceipt(id);
|
await receiptStore.deleteReceipt(id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete goods receipt", error);
|
console.error("Failed to delete goods receipt", error);
|
||||||
}
|
}
|
||||||
@ -103,7 +113,7 @@ const loadGoodsReceipts = () => {
|
|||||||
search: searchQuery.value || undefined,
|
search: searchQuery.value || undefined,
|
||||||
status: statusFilter.value || undefined,
|
status: statusFilter.value || undefined,
|
||||||
};
|
};
|
||||||
goodsReceiptStore.fetchGoodsReceipts(params);
|
receiptStore.fetchReceipts(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@ -17,10 +17,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<new-reception-form
|
<new-reception-form
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:purchase-orders="purchaseOrders"
|
:purchase-orders="purchaseOrders"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
@cancel="goBack"
|
@cancel="goBack"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,169 +1,267 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid py-4" v-if="goodsReceipt">
|
<div v-if="loading" class="text-center py-5">
|
||||||
<div class="d-sm-flex justify-content-between mb-4">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<div>
|
<span class="visually-hidden">Loading...</span>
|
||||||
<h5 class="mb-0">Réception de Marchandise: {{ goodsReceipt.receipt_number }}</h5>
|
|
||||||
<p class="text-sm mb-0">
|
|
||||||
Créée le {{ formatDate(goodsReceipt.created_at) }} -
|
|
||||||
<soft-badge :color="getStatusColor(goodsReceipt.status)" variant="gradient">
|
|
||||||
{{ getStatusLabel(goodsReceipt.status) }}
|
|
||||||
</soft-badge>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-sm-0 mt-3">
|
|
||||||
<soft-button color="secondary" variant="gradient" class="me-2" @click="handleBack">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i> Retour
|
|
||||||
</soft-button>
|
|
||||||
<soft-button color="info" variant="gradient" class="me-2" @click="handleEdit">
|
|
||||||
<i class="fas fa-edit me-2"></i> Modifier
|
|
||||||
</soft-button>
|
|
||||||
<soft-button color="danger" variant="gradient" @click="handleDelete">
|
|
||||||
<i class="fas fa-trash me-2"></i> Supprimer
|
|
||||||
</soft-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header pb-0">
|
|
||||||
<h6>Informations Générales</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="text-xs font-weight-bold">Numéro de Réception</label>
|
|
||||||
<p class="text-sm">{{ goodsReceipt.receipt_number }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="text-xs font-weight-bold">Date de Réception</label>
|
|
||||||
<p class="text-sm">{{ formatDate(goodsReceipt.receipt_date) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="text-xs font-weight-bold">Commande Fournisseur</label>
|
|
||||||
<p class="text-sm">
|
|
||||||
{{ goodsReceipt.purchase_order?.po_number || goodsReceipt.purchase_order_id }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="text-xs font-weight-bold">Entrepôt de Destination</label>
|
|
||||||
<p class="text-sm">{{ goodsReceipt.warehouse?.name || '-' }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="text-xs font-weight-bold">Notes</label>
|
|
||||||
<p class="text-sm">{{ goodsReceipt.notes || 'Aucune note' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header pb-0">
|
|
||||||
<h6>Lignes de Réception</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-flush">
|
|
||||||
<thead class="thead-light">
|
|
||||||
<tr>
|
|
||||||
<th>Produit</th>
|
|
||||||
<th>Conditionnement</th>
|
|
||||||
<th>Colis</th>
|
|
||||||
<th>Unités</th>
|
|
||||||
<th>Prix Unitaire</th>
|
|
||||||
<th>TVA</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="line in goodsReceipt.lines" :key="line.id">
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div>
|
|
||||||
<p class="text-xs font-weight-bold mb-0">
|
|
||||||
{{ line.product?.nom || 'Produit ' + line.product_id }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-secondary mb-0">
|
|
||||||
{{ line.product?.reference || '' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs">
|
|
||||||
{{ line.packaging?.name || 'Unité' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-xs">
|
|
||||||
{{ line.packages_qty_received || '-' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-xs">
|
|
||||||
{{ line.units_qty_received || '-' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-xs">
|
|
||||||
{{ line.unit_price ? formatCurrency(line.unit_price) : '-' }}
|
|
||||||
</td>
|
|
||||||
<td class="text-xs">
|
|
||||||
{{ line.tva_rate ? line.tva_rate.name + ' (' + line.tva_rate.rate + '%)' : '-' }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="!goodsReceipt.lines || goodsReceipt.lines.length === 0">
|
|
||||||
<td colspan="6" class="text-center text-muted py-4">
|
|
||||||
Aucune ligne dans cette réception.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="container-fluid py-4">
|
|
||||||
<div class="d-flex justify-content-center">
|
<div v-else-if="error" class="text-center py-5 text-danger">
|
||||||
<div class="spinner-border" role="status">
|
{{ error }}
|
||||||
<span class="visually-hidden">Chargement...</span>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="goodsReceipt" class="container-fluid py-4">
|
||||||
|
<div class="odoo-toolbar">
|
||||||
|
<div class="statusbar-wrapper">
|
||||||
|
<button
|
||||||
|
v-for="status in availableStatuses"
|
||||||
|
:key="status"
|
||||||
|
type="button"
|
||||||
|
class="status-step"
|
||||||
|
:class="{ active: status === goodsReceipt.status }"
|
||||||
|
:disabled="true"
|
||||||
|
>
|
||||||
|
<i :class="getStatusIcon(status)"></i>
|
||||||
|
<span>{{ getStatusLabel(status) }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<soft-button
|
||||||
|
color="success"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
:disabled="!canValidate || isUpdatingStatus"
|
||||||
|
@click="handleValidate"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check me-2"></i>
|
||||||
|
Valider réception
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="warning"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
:disabled="!canSetDraft || isUpdatingStatus"
|
||||||
|
@click="handleSetDraft"
|
||||||
|
>
|
||||||
|
<i class="fas fa-undo me-2"></i>
|
||||||
|
Remettre en brouillon
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
@click="handleBack"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>
|
||||||
|
Retour
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
@click="handleEdit"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit me-2"></i>
|
||||||
|
Modifier
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
:disabled="isUpdatingStatus"
|
||||||
|
@click="handleDelete"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash me-2"></i>
|
||||||
|
Supprimer
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="commande-detail mt-3">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-truck-loading"></i>
|
||||||
|
Informations générales
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Numéro réception</label>
|
||||||
|
<div class="info-value">{{ goodsReceipt.receipt_number }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Date réception</label>
|
||||||
|
<div class="info-value">{{ formatDate(goodsReceipt.receipt_date) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<div class="status-badge" :class="getStatusClass(goodsReceipt.status)">
|
||||||
|
<i :class="getStatusIcon(goodsReceipt.status)"></i>
|
||||||
|
{{ getStatusLabel(goodsReceipt.status) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Commande fournisseur</label>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ goodsReceipt.purchase_order?.po_number || goodsReceipt.purchase_order_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Entrepôt</label>
|
||||||
|
<div class="info-value">{{ goodsReceipt.warehouse?.name || "-" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-boxes"></i>
|
||||||
|
Lignes de réception
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Produit</th>
|
||||||
|
<th>Conditionnement</th>
|
||||||
|
<th>Colis</th>
|
||||||
|
<th>Unités</th>
|
||||||
|
<th>Prix Unitaire</th>
|
||||||
|
<th>TVA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="line in receiptLines" :key="line.id">
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="text-sm fw-bold">{{ line.product?.nom || `Produit #${line.product_id}` }}</span>
|
||||||
|
<span class="text-xs text-secondary">{{ line.product?.reference || "-" }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">{{ line.packaging?.name || "Unité" }}</td>
|
||||||
|
<td class="text-sm">{{ line.packages_qty_received || "-" }}</td>
|
||||||
|
<td class="text-sm">{{ line.units_qty_received || "-" }}</td>
|
||||||
|
<td class="text-sm">{{ line.unit_price ? formatCurrency(line.unit_price) : "-" }}</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
{{ line.tva_rate ? `${line.tva_rate.name} (${line.tva_rate.rate}%)` : "-" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="receiptLines.length === 0">
|
||||||
|
<td colspan="6" class="text-center text-muted py-4">
|
||||||
|
Aucune ligne dans cette réception.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, computed } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import SoftBadge from "@/components/SoftBadge.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const goodsReceiptStore = useGoodsReceiptStore();
|
const goodsReceiptStore = useGoodsReceiptStore();
|
||||||
const { currentGoodsReceipt: goodsReceipt, loading } = storeToRefs(goodsReceiptStore);
|
|
||||||
|
const { currentGoodsReceipt: goodsReceipt, loading, error } = storeToRefs(goodsReceiptStore);
|
||||||
|
|
||||||
|
const isUpdatingStatus = ref(false);
|
||||||
|
|
||||||
|
const availableStatuses = ["draft", "posted"];
|
||||||
|
|
||||||
|
const canValidate = computed(() => goodsReceipt.value?.status === "draft");
|
||||||
|
const canSetDraft = computed(() => goodsReceipt.value?.status === "posted");
|
||||||
|
const receiptLines = computed(() => {
|
||||||
|
const lines = goodsReceipt.value?.lines;
|
||||||
|
if (Array.isArray(lines)) {
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
if (lines && Array.isArray(lines.data)) {
|
||||||
|
return lines.data;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return '-';
|
if (!dateString) return "-";
|
||||||
const date = new Date(dateString);
|
return new Date(dateString).toLocaleDateString("fr-FR", {
|
||||||
return date.toLocaleDateString('fr-FR');
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (value) => {
|
const formatCurrency = (value) => {
|
||||||
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
};
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
const getStatusColor = (status) => {
|
}).format(value || 0);
|
||||||
switch (status) {
|
|
||||||
case 'draft': return 'secondary';
|
|
||||||
case 'posted': return 'success';
|
|
||||||
default: return 'info';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status) => {
|
const getStatusLabel = (status) => {
|
||||||
switch (status) {
|
const labels = {
|
||||||
case 'draft': return 'Brouillon';
|
draft: "Brouillon",
|
||||||
case 'posted': return 'Validée';
|
posted: "Validée",
|
||||||
default: return status;
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status) => {
|
||||||
|
const icons = {
|
||||||
|
draft: "fas fa-file-alt",
|
||||||
|
posted: "fas fa-check-circle",
|
||||||
|
};
|
||||||
|
return icons[status] || "fas fa-question-circle";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const classes = {
|
||||||
|
draft: "status-draft",
|
||||||
|
posted: "status-confirmed",
|
||||||
|
};
|
||||||
|
return classes[status] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeStatus = async (newStatus) => {
|
||||||
|
if (!goodsReceipt.value || goodsReceipt.value.status === newStatus) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isUpdatingStatus.value = true;
|
||||||
|
await goodsReceiptStore.updateGoodsReceipt({
|
||||||
|
id: goodsReceipt.value.id,
|
||||||
|
status: newStatus,
|
||||||
|
});
|
||||||
|
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update receipt status", e);
|
||||||
|
} finally {
|
||||||
|
isUpdatingStatus.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleValidate = async () => {
|
||||||
|
await changeStatus("posted");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetDraft = async () => {
|
||||||
|
await changeStatus("draft");
|
||||||
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
router.push("/stock/receptions");
|
router.push("/stock/receptions");
|
||||||
};
|
};
|
||||||
@ -173,13 +271,14 @@ const handleEdit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
|
if (!confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
|
||||||
try {
|
return;
|
||||||
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
|
}
|
||||||
router.push("/stock/receptions");
|
try {
|
||||||
} catch (error) {
|
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
|
||||||
console.error("Failed to delete goods receipt", error);
|
router.push("/stock/receptions");
|
||||||
}
|
} catch (e) {
|
||||||
|
console.error("Failed to delete goods receipt", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -187,3 +286,99 @@ onMounted(async () => {
|
|||||||
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
|
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.odoo-toolbar {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step {
|
||||||
|
border: 1px solid #dfe3e8;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #67748e;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step.active {
|
||||||
|
background: #2dce89;
|
||||||
|
border-color: #2dce89;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commande-detail {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #344767;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-draft {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-confirmed {
|
||||||
|
background: #d1f7e3;
|
||||||
|
color: #0a7a43;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,38 +1,423 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="loading" class="text-center py-5">
|
<div class="py-4 container-fluid">
|
||||||
<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="row">
|
||||||
<div class="col-lg-8 mx-auto">
|
<div class="col-lg-6">
|
||||||
<div class="card mb-4">
|
<h4>
|
||||||
<div class="card-header pb-0 p-3">
|
{{ isEditMode ? "Modifier l'entrepôt" : "Détails de l'entrepôt" }}
|
||||||
<div class="row">
|
</h4>
|
||||||
<div class="col-md-8 d-flex align-items-center">
|
<p>
|
||||||
<h6 class="mb-0">Détails de l'entrepôt: {{ warehouse.name }}</h6>
|
{{
|
||||||
|
isEditMode
|
||||||
|
? "Modifiez les informations de l'entrepôt ci-dessous."
|
||||||
|
: "Informations détaillées, produits stockés et mouvements."
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-right col-lg-6 d-flex flex-column justify-content-center"
|
||||||
|
>
|
||||||
|
<div class="w-100 mt-lg-0">
|
||||||
|
<div v-if="!isEditMode" class="d-flex align-items-center gap-2 w-100">
|
||||||
|
<div class="flex-grow-1 d-flex justify-content-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
@click="handleBack"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>
|
||||||
|
Retour à la liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 ms-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-2 mb-0 btn bg-gradient-info"
|
||||||
|
@click="toggleEditMode"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit me-2"></i>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-2 mb-0 btn bg-gradient-danger"
|
||||||
|
:disabled="deleting"
|
||||||
|
@click="handleDelete"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash me-2"></i>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="d-flex gap-2 justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-2 mb-0 btn bg-gradient-secondary"
|
||||||
|
@click="cancelEdit"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-2 mb-0 btn bg-gradient-success"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="saveWarehouse"
|
||||||
|
>
|
||||||
|
<i v-if="saving" class="fas fa-spinner fa-spin me-2"></i>
|
||||||
|
<i v-else class="fas fa-save me-2"></i>
|
||||||
|
Sauvegarder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p>Chargement des informations de l'entrepôt...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="error-container">
|
||||||
|
<div class="error-message">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-2x text-danger mb-3"></i>
|
||||||
|
<h5>Erreur de chargement</h5>
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary btn-sm"
|
||||||
|
@click="loadWarehouseData"
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="warehouse" class="warehouse-details-content mt-3">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-xl-3 col-lg-4">
|
||||||
|
<div class="card warehouse-side-nav-card">
|
||||||
|
<div class="card-body pb-2">
|
||||||
|
<div class="warehouse-sidebar-profile">
|
||||||
|
<div class="warehouse-sidebar-icon">
|
||||||
|
<i class="fas fa-warehouse"></i>
|
||||||
|
</div>
|
||||||
|
<h6 class="warehouse-sidebar-title text-center mb-1">
|
||||||
|
{{ warehouse.name }}
|
||||||
|
</h6>
|
||||||
|
<p class="warehouse-sidebar-reference text-center mb-0">
|
||||||
|
{{ warehouse.city || "Ville non renseignée" }}
|
||||||
|
</p>
|
||||||
|
<div class="warehouse-sidebar-badges mt-3">
|
||||||
|
<span class="badge bg-primary">{{
|
||||||
|
warehouse.country_code || "--"
|
||||||
|
}}</span>
|
||||||
|
<span class="badge bg-info text-dark"
|
||||||
|
>{{ warehouseProducts.length }} produits</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
</div>
|
||||||
<soft-button color="info" variant="outline" size="sm" @click="handleEdit">
|
|
||||||
<i class="fas fa-user-edit me-2"></i> Modifier
|
<hr class="horizontal dark my-2 mx-3" />
|
||||||
</soft-button>
|
|
||||||
|
<div class="card-body pt-2">
|
||||||
|
<ul class="nav nav-pills flex-column warehouse-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ active: activeTab === 'details' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="activeTab = 'details'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
<span class="text-sm">Informations</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item pt-2">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ active: activeTab === 'products' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="activeTab = 'products'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-boxes me-2"></i>
|
||||||
|
<span class="text-sm">Produits</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item pt-2">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ active: activeTab === 'movements' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="activeTab = 'movements'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exchange-alt me-2"></i>
|
||||||
|
<span class="text-sm">Mouvements</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-9 col-lg-8">
|
||||||
|
<div v-show="activeTab === 'details'">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-gradient-primary text-white">
|
||||||
|
<h5 class="mb-0">Informations de l'entrepôt</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label>Nom de l'entrepôt *</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditMode"
|
||||||
|
v-model="formData.name"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nom de l'entrepôt"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static">
|
||||||
|
{{ warehouse.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label>Pays</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditMode"
|
||||||
|
v-model="formData.country_code"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
maxlength="2"
|
||||||
|
placeholder="FR"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static">
|
||||||
|
{{ warehouse.country_code || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label>Adresse 1</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditMode"
|
||||||
|
v-model="formData.address_line1"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Adresse ligne 1"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static">
|
||||||
|
{{ warehouse.address_line1 || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label>Adresse 2</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditMode"
|
||||||
|
v-model="formData.address_line2"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Adresse ligne 2"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static">
|
||||||
|
{{ warehouse.address_line2 || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label>Code postal</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditMode"
|
||||||
|
v-model="formData.postal_code"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Code postal"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static">
|
||||||
|
{{ warehouse.postal_code || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label>Ville</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditMode"
|
||||||
|
v-model="formData.city"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Ville"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static">
|
||||||
|
{{ warehouse.city || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-3">
|
|
||||||
<warehouse-detail-info :warehouse="warehouse" />
|
<div v-show="activeTab === 'products'">
|
||||||
<hr class="horizontal dark my-4" />
|
<div class="card">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="card-body">
|
||||||
<soft-button color="secondary" variant="gradient" @click="handleBack">
|
<h5 class="font-weight-bolder mb-4">
|
||||||
<i class="fas fa-arrow-left me-2"></i> Retour à la liste
|
Produits dans l'entrepôt
|
||||||
</soft-button>
|
</h5>
|
||||||
<soft-button color="danger" variant="outline" @click="handleDelete">
|
|
||||||
<i class="fas fa-trash me-2"></i> Supprimer
|
<div v-if="warehouseProducts.length === 0" class="text-muted">
|
||||||
</soft-button>
|
Aucun produit trouvé dans cet entrepôt.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table class="table align-items-center mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Produit
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Quantité
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Stock de sécurité
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder text-end"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in warehouseProducts" :key="item.id">
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="text-sm fw-bold">{{
|
||||||
|
item.product?.nom || `Produit #${item.product_id}`
|
||||||
|
}}</span>
|
||||||
|
<span class="text-xs text-secondary"
|
||||||
|
>Réf: {{ item.product?.reference || "-" }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">{{ item.qty_on_hand_base }}</td>
|
||||||
|
<td class="text-sm">{{ item.safety_stock_base }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-link text-info btn-sm mb-0"
|
||||||
|
@click="goToProduct(item.product_id)"
|
||||||
|
>
|
||||||
|
Voir produit
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'movements'">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between align-items-center mb-4"
|
||||||
|
>
|
||||||
|
<h5 class="font-weight-bolder mb-0">
|
||||||
|
Mouvements de stock de l'entrepôt
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-primary mb-0"
|
||||||
|
@click="goToStockPage"
|
||||||
|
>
|
||||||
|
<i class="fas fa-external-link-alt me-1"></i>
|
||||||
|
Menu stock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="warehouseMovements.length === 0" class="text-muted">
|
||||||
|
Aucun mouvement pour cet entrepôt.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table class="table align-items-center mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Produit
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Entrée / Sortie
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Quantité
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="movement in warehouseMovements"
|
||||||
|
:key="movement.id"
|
||||||
|
>
|
||||||
|
<td class="text-sm">
|
||||||
|
{{
|
||||||
|
formatDate(movement.moved_at || movement.created_at)
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">{{ movement.move_type || "-" }}</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
{{
|
||||||
|
movement.product?.nom ||
|
||||||
|
`Produit #${movement.product_id}`
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
:class="
|
||||||
|
isIncoming(movement)
|
||||||
|
? 'bg-success'
|
||||||
|
: 'bg-warning text-dark'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ isIncoming(movement) ? "Entrée" : "Sortie" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">{{ movement.qty_base }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,11 +427,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, defineProps } from "vue";
|
import { computed, defineProps, onMounted, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { useStockStore } from "@/stores/stockStore";
|
||||||
import { useWarehouseStore } from "@/stores/warehouseStore";
|
import { useWarehouseStore } from "@/stores/warehouseStore";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
|
||||||
import WarehouseDetailInfo from "@/components/molecules/Stock/WarehouseDetailInfo.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
warehouseId: {
|
warehouseId: {
|
||||||
@ -57,39 +441,242 @@ const props = defineProps({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const warehouseStore = useWarehouseStore();
|
const warehouseStore = useWarehouseStore();
|
||||||
|
const stockStore = useStockStore();
|
||||||
|
|
||||||
const warehouse = ref(null);
|
const warehouse = ref(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
const saving = ref(false);
|
||||||
|
const deleting = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
|
const activeTab = ref("details");
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
const formData = ref({
|
||||||
|
name: "",
|
||||||
|
address_line1: "",
|
||||||
|
address_line2: "",
|
||||||
|
postal_code: "",
|
||||||
|
city: "",
|
||||||
|
country_code: "FR",
|
||||||
|
});
|
||||||
|
|
||||||
|
const numericWarehouseId = computed(() => Number(props.warehouseId));
|
||||||
|
|
||||||
|
const warehouseProducts = computed(() => {
|
||||||
|
return stockStore.stockItems.filter(
|
||||||
|
(item) => Number(item.warehouse_id) === numericWarehouseId.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const warehouseMovements = computed(() => {
|
||||||
|
return stockStore.stockMoves.filter(
|
||||||
|
(move) =>
|
||||||
|
Number(move.from_warehouse_id) === numericWarehouseId.value ||
|
||||||
|
Number(move.to_warehouse_id) === numericWarehouseId.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initializeFormData = (value) => {
|
||||||
|
formData.value = {
|
||||||
|
name: value?.name || "",
|
||||||
|
address_line1: value?.address_line1 || "",
|
||||||
|
address_line2: value?.address_line2 || "",
|
||||||
|
postal_code: value?.postal_code || "",
|
||||||
|
city: value?.city || "",
|
||||||
|
country_code: value?.country_code || "FR",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadWarehouseData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const fetchedWarehouse = await warehouseStore.fetchWarehouse(props.warehouseId);
|
const [fetchedWarehouse] = await Promise.all([
|
||||||
|
warehouseStore.fetchWarehouse(numericWarehouseId.value),
|
||||||
|
stockStore.fetchStockItems(),
|
||||||
|
stockStore.fetchStockMoves(),
|
||||||
|
]);
|
||||||
|
|
||||||
warehouse.value = fetchedWarehouse;
|
warehouse.value = fetchedWarehouse;
|
||||||
|
initializeFormData(fetchedWarehouse);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = "Impossible de charger l'entrepôt.";
|
error.value = "Impossible de charger les informations de l'entrepôt.";
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
const toggleEditMode = () => {
|
||||||
router.push(`/stock/warehouses/${props.warehouseId}/edit`);
|
isEditMode.value = true;
|
||||||
|
initializeFormData(warehouse.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
isEditMode.value = false;
|
||||||
|
initializeFormData(warehouse.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveWarehouse = async () => {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const updated = await warehouseStore.updateWarehouse(
|
||||||
|
numericWarehouseId.value,
|
||||||
|
{
|
||||||
|
...formData.value,
|
||||||
|
country_code: (formData.value.country_code || "FR").toUpperCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
initializeFormData(updated);
|
||||||
|
isEditMode.value = false;
|
||||||
|
await loadWarehouseData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update warehouse", e);
|
||||||
|
error.value =
|
||||||
|
e?.response?.data?.message ||
|
||||||
|
"Erreur lors de la mise à jour de l'entrepôt.";
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
router.push("/stock/warehouses");
|
router.push("/stock/warehouses");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goToProduct = (productId) => {
|
||||||
|
router.push(`/stock/produits/details/${productId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToStockPage = () => {
|
||||||
|
router.push("/stock");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (value) => {
|
||||||
|
if (!value) return "-";
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
return date.toLocaleDateString("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIncoming = (movement) => {
|
||||||
|
return Number(movement.to_warehouse_id) === numericWarehouseId.value;
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cet entrepôt ?")) {
|
if (!confirm("Êtes-vous sûr de vouloir supprimer cet entrepôt ?")) {
|
||||||
try {
|
return;
|
||||||
await warehouseStore.deleteWarehouse(props.warehouseId);
|
}
|
||||||
router.push("/stock/warehouses");
|
|
||||||
} catch (e) {
|
deleting.value = true;
|
||||||
console.error("Failed to delete warehouse", e);
|
try {
|
||||||
}
|
await warehouseStore.deleteWarehouse(numericWarehouseId.value);
|
||||||
|
router.push("/stock/warehouses");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete warehouse", e);
|
||||||
|
error.value =
|
||||||
|
e?.response?.data?.message || "Erreur lors de la suppression.";
|
||||||
|
} finally {
|
||||||
|
deleting.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadWarehouseData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-container,
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 300px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner,
|
||||||
|
.error-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-static {
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-side-nav-card {
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-sidebar-profile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-sidebar-icon {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 2rem;
|
||||||
|
background: #f8f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-sidebar-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-sidebar-reference {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-sidebar-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-nav .nav-link {
|
||||||
|
color: #6b7280;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-nav .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0.25rem 0.75rem rgba(102, 126, 234, 0.35);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="col-lg-8 mx-auto">
|
<div class="col-lg-8 mx-auto">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header pb-0 p-3">
|
<div class="card-header pb-0 p-3">
|
||||||
<h6 class="mb-0">{{ isEdit ? 'Modifier' : 'Nouvel' }} Entrepôt</h6>
|
<h6 class="mb-0">{{ isEdit ? "Modifier" : "Nouvel" }} Entrepôt</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<warehouse-form
|
<warehouse-form
|
||||||
@ -83,7 +83,10 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
router.push("/stock/warehouses");
|
router.push("/stock/warehouses");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.response?.data?.message || e.message || "Une erreur est survenue lors de l'enregistrement.";
|
error.value =
|
||||||
|
e.response?.data?.message ||
|
||||||
|
e.message ||
|
||||||
|
"Une erreur est survenue lors de l'enregistrement.";
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
|
|||||||
@ -15,7 +15,9 @@
|
|||||||
<tr v-for="line in lines" :key="line.id">
|
<tr v-for="line in lines" :key="line.id">
|
||||||
<td class="text-sm">{{ line.designation }}</td>
|
<td class="text-sm">{{ line.designation }}</td>
|
||||||
<td class="text-center text-sm">{{ line.quantity }}</td>
|
<td class="text-center text-sm">{{ line.quantity }}</td>
|
||||||
<td class="text-end text-sm">{{ formatCurrency(line.price_ht) }}</td>
|
<td class="text-end text-sm">
|
||||||
|
{{ formatCurrency(line.price_ht) }}
|
||||||
|
</td>
|
||||||
<td class="text-end text-sm font-weight-bold">
|
<td class="text-end text-sm font-weight-bold">
|
||||||
{{ formatCurrency(line.total_ht) }}
|
{{ formatCurrency(line.total_ht) }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,36 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="submitForm" class="purchase-form">
|
<form class="purchase-form" @submit.prevent="submitForm">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<i class="fas fa-file-invoice"></i>
|
<i class="fas fa-file-invoice"></i>
|
||||||
Informations générales
|
Informations générales
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 1: Fournisseur, Date -->
|
<!-- Row 1: Fournisseur, Date -->
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-6 position-relative supplier-search-container">
|
<div class="col-md-6 position-relative supplier-search-container">
|
||||||
<label class="form-label">Fournisseur <span class="text-danger">*</span></label>
|
<label class="form-label"
|
||||||
|
>Fournisseur <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model="supplierSearchQuery"
|
v-model="supplierSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher un fournisseur..."
|
placeholder="Rechercher un fournisseur..."
|
||||||
@input="handleSupplierSearch"
|
|
||||||
@focus="showSupplierResults = true"
|
|
||||||
required
|
required
|
||||||
class="search-input"
|
class="search-input"
|
||||||
|
@input="handleSupplierSearch"
|
||||||
|
@focus="showSupplierResults = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Results Dropdown -->
|
<!-- Search Results Dropdown -->
|
||||||
<div
|
<div
|
||||||
v-if="showSupplierResults && (supplierSearchResults.length > 0 || isSearchingSuppliers)"
|
v-if="
|
||||||
|
showSupplierResults &&
|
||||||
|
(supplierSearchResults.length > 0 || isSearchingSuppliers)
|
||||||
|
"
|
||||||
class="search-dropdown"
|
class="search-dropdown"
|
||||||
>
|
>
|
||||||
<div v-if="isSearchingSuppliers" class="dropdown-loading">
|
<div v-if="isSearchingSuppliers" class="dropdown-loading">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,20 +52,19 @@
|
|||||||
>
|
>
|
||||||
<span class="item-name">{{ supplier.name }}</span>
|
<span class="item-name">{{ supplier.name }}</span>
|
||||||
<span class="item-details">
|
<span class="item-details">
|
||||||
{{ supplier.email || 'Pas d\'email' }} • {{ supplier.billing_address?.city || 'Ville non spécifiée' }}
|
{{ supplier.email || "Pas d'email" }} •
|
||||||
|
{{ supplier.billing_address?.city || "Ville non spécifiée" }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Date commande <span class="text-danger">*</span></label>
|
<label class="form-label"
|
||||||
<soft-input
|
>Date commande <span class="text-danger">*</span></label
|
||||||
v-model="formData.date"
|
>
|
||||||
type="date"
|
<soft-input v-model="formData.date" type="date" required />
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -113,8 +120,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="addLine"
|
|
||||||
class="add-btn"
|
class="add-btn"
|
||||||
|
@click="addLine"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i> Ajouter ligne
|
<i class="fas fa-plus"></i> Ajouter ligne
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -128,31 +135,42 @@
|
|||||||
>
|
>
|
||||||
<div class="row g-2 align-items-end">
|
<div class="row g-2 align-items-end">
|
||||||
<div class="col-md-4 position-relative product-search-container">
|
<div class="col-md-4 position-relative product-search-container">
|
||||||
<label class="form-label text-xs">Article <span class="text-danger">*</span></label>
|
<label class="form-label text-xs"
|
||||||
|
>Article <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model="line.searchQuery"
|
v-model="line.searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher un article..."
|
placeholder="Rechercher un article..."
|
||||||
@input="handleProductSearch(index)"
|
|
||||||
@focus="activeLineIndex = index; showProductResults = true"
|
|
||||||
required
|
required
|
||||||
class="search-input"
|
class="search-input"
|
||||||
|
@input="handleProductSearch(index)"
|
||||||
|
@focus="
|
||||||
|
activeLineIndex = index;
|
||||||
|
showProductResults = true;
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Search Results Dropdown -->
|
<!-- Product Search Results Dropdown -->
|
||||||
<div
|
<div
|
||||||
v-show="showProductResults && activeLineIndex === index"
|
v-show="showProductResults && activeLineIndex === index"
|
||||||
class="search-dropdown product-dropdown"
|
class="search-dropdown product-dropdown"
|
||||||
>
|
>
|
||||||
<div v-if="isSearchingProducts" class="dropdown-loading">
|
<div v-if="isSearchingProducts" class="dropdown-loading">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="productSearchResults.length === 0" class="dropdown-empty">
|
<div
|
||||||
|
v-else-if="productSearchResults.length === 0"
|
||||||
|
class="dropdown-empty"
|
||||||
|
>
|
||||||
Aucun produit trouvé
|
Aucun produit trouvé
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -165,15 +183,19 @@
|
|||||||
>
|
>
|
||||||
<span class="item-name">{{ product.nom }}</span>
|
<span class="item-name">{{ product.nom }}</span>
|
||||||
<span class="item-details">
|
<span class="item-details">
|
||||||
Réf: {{ product.reference }} • Stock: {{ product.stock_actuel }} {{ product.unite }} • {{ formatCurrency(product.prix_unitaire) }}
|
Réf: {{ product.reference }} • Stock:
|
||||||
|
{{ product.stock_actuel }} {{ product.unite }} •
|
||||||
|
{{ formatCurrency(product.prix_unitaire) }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label text-xs">Désignation <span class="text-danger">*</span></label>
|
<label class="form-label text-xs"
|
||||||
|
>Désignation <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model="line.designation"
|
v-model="line.designation"
|
||||||
type="text"
|
type="text"
|
||||||
@ -181,9 +203,11 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Qté <span class="text-danger">*</span></label>
|
<label class="form-label text-xs"
|
||||||
|
>Qté <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model.number="line.quantity"
|
v-model.number="line.quantity"
|
||||||
type="number"
|
type="number"
|
||||||
@ -192,9 +216,11 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Prix HT <span class="text-danger">*</span></label>
|
<label class="form-label text-xs"
|
||||||
|
>Prix HT <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model.number="line.priceHt"
|
v-model.number="line.priceHt"
|
||||||
type="number"
|
type="number"
|
||||||
@ -203,14 +229,14 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-1 d-flex flex-column align-items-end gap-2">
|
<div class="col-md-1 d-flex flex-column align-items-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-delete"
|
class="btn-delete"
|
||||||
@click="removeLine(index)"
|
|
||||||
:disabled="formData.lines.length === 1"
|
:disabled="formData.lines.length === 1"
|
||||||
title="Supprimer la ligne"
|
title="Supprimer la ligne"
|
||||||
|
@click="removeLine(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -228,15 +254,21 @@
|
|||||||
<div class="totals-content">
|
<div class="totals-content">
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
<span class="total-label">Total HT</span>
|
<span class="total-label">Total HT</span>
|
||||||
<span class="total-value">{{ formatCurrency(calculateTotalHt()) }}</span>
|
<span class="total-value">{{
|
||||||
|
formatCurrency(calculateTotalHt())
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
<span class="total-label">TVA (20%)</span>
|
<span class="total-label">TVA (20%)</span>
|
||||||
<span class="total-value">{{ formatCurrency(calculateTotalTva()) }}</span>
|
<span class="total-value">{{
|
||||||
|
formatCurrency(calculateTotalTva())
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="total-row total-final">
|
<div class="total-row total-final">
|
||||||
<span class="total-label">Total TTC</span>
|
<span class="total-label">Total TTC</span>
|
||||||
<span class="total-amount">{{ formatCurrency(calculateTotalTtc()) }}</span>
|
<span class="total-amount">{{
|
||||||
|
formatCurrency(calculateTotalTtc())
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -247,16 +279,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="cancelForm"
|
|
||||||
class="btn-cancel"
|
class="btn-cancel"
|
||||||
|
@click="cancelForm"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i> Annuler
|
<i class="fas fa-times"></i> Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button
|
<soft-button type="submit" color="success" class="btn-submit">
|
||||||
type="submit"
|
|
||||||
color="success"
|
|
||||||
class="btn-submit"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check"></i> Créer la commande
|
<i class="fas fa-check"></i> Créer la commande
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
@ -301,7 +329,9 @@ const handleSupplierSearch = () => {
|
|||||||
isSearchingSuppliers.value = true;
|
isSearchingSuppliers.value = true;
|
||||||
showSupplierResults.value = true;
|
showSupplierResults.value = true;
|
||||||
try {
|
try {
|
||||||
const results = await FournisseurService.searchFournisseurs(supplierSearchQuery.value);
|
const results = await FournisseurService.searchFournisseurs(
|
||||||
|
supplierSearchQuery.value
|
||||||
|
);
|
||||||
// Handle both direct array or paginated object with data property
|
// Handle both direct array or paginated object with data property
|
||||||
let actualResults = [];
|
let actualResults = [];
|
||||||
if (Array.isArray(results)) {
|
if (Array.isArray(results)) {
|
||||||
@ -309,7 +339,7 @@ const handleSupplierSearch = () => {
|
|||||||
} else if (results && Array.isArray(results.data)) {
|
} else if (results && Array.isArray(results.data)) {
|
||||||
actualResults = results.data;
|
actualResults = results.data;
|
||||||
}
|
}
|
||||||
supplierSearchResults.value = actualResults.filter(s => s && s.id);
|
supplierSearchResults.value = actualResults.filter((s) => s && s.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error searching suppliers:", error);
|
console.error("Error searching suppliers:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -321,13 +351,15 @@ const handleSupplierSearch = () => {
|
|||||||
const selectSupplier = (supplier) => {
|
const selectSupplier = (supplier) => {
|
||||||
if (!supplier || !supplier.id) return;
|
if (!supplier || !supplier.id) return;
|
||||||
if (searchTimeout) clearTimeout(searchTimeout);
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
|
|
||||||
formData.value.supplierId = supplier.id;
|
formData.value.supplierId = supplier.id;
|
||||||
formData.value.supplierName = supplier.name;
|
formData.value.supplierName = supplier.name;
|
||||||
formData.value.supplierAddress = supplier.billing_address ?
|
formData.value.supplierAddress = supplier.billing_address
|
||||||
`${supplier.billing_address.line1 || ''} ${supplier.billing_address.postal_code || ''} ${supplier.billing_address.city || ''}` :
|
? `${supplier.billing_address.line1 || ""} ${
|
||||||
"À déterminer";
|
supplier.billing_address.postal_code || ""
|
||||||
|
} ${supplier.billing_address.city || ""}`
|
||||||
|
: "À déterminer";
|
||||||
|
|
||||||
supplierSearchQuery.value = supplier.name;
|
supplierSearchQuery.value = supplier.name;
|
||||||
showSupplierResults.value = false;
|
showSupplierResults.value = false;
|
||||||
};
|
};
|
||||||
@ -344,7 +376,6 @@ const handleProductSearch = (index) => {
|
|||||||
const query = formData.value.lines[index].searchQuery;
|
const query = formData.value.lines[index].searchQuery;
|
||||||
console.log(query.length);
|
console.log(query.length);
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
|
|
||||||
productSearchResults.value = [];
|
productSearchResults.value = [];
|
||||||
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
||||||
isSearchingProducts.value = false;
|
isSearchingProducts.value = false;
|
||||||
@ -362,10 +393,11 @@ const handleProductSearch = (index) => {
|
|||||||
try {
|
try {
|
||||||
const response = await productService.searchProducts(query);
|
const response = await productService.searchProducts(query);
|
||||||
// Double check if this is still the active line and query, and line still exists
|
// Double check if this is still the active line and query, and line still exists
|
||||||
if (activeLineIndex.value === index &&
|
if (
|
||||||
formData.value.lines[index] &&
|
activeLineIndex.value === index &&
|
||||||
formData.value.lines[index].searchQuery === query) {
|
formData.value.lines[index] &&
|
||||||
|
formData.value.lines[index].searchQuery === query
|
||||||
|
) {
|
||||||
// Handle paginated response: the array is in response.data.data
|
// Handle paginated response: the array is in response.data.data
|
||||||
// Handle non-paginated: the array is in response.data
|
// Handle non-paginated: the array is in response.data
|
||||||
let results = [];
|
let results = [];
|
||||||
@ -376,8 +408,8 @@ const handleProductSearch = (index) => {
|
|||||||
results = response.data.data;
|
results = response.data.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
productSearchResults.value = results.filter(p => p && p.id);
|
productSearchResults.value = results.filter((p) => p && p.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error searching products:", error);
|
console.error("Error searching products:", error);
|
||||||
@ -396,42 +428,46 @@ const selectProduct = (index, product) => {
|
|||||||
|
|
||||||
const line = formData.value.lines[index];
|
const line = formData.value.lines[index];
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
|
|
||||||
line.productId = product.id;
|
line.productId = product.id;
|
||||||
line.searchQuery = product.nom;
|
line.searchQuery = product.nom;
|
||||||
line.designation = product.nom;
|
line.designation = product.nom;
|
||||||
line.priceHt = product.prix_unitaire;
|
line.priceHt = product.prix_unitaire;
|
||||||
|
|
||||||
showProductResults.value = false;
|
showProductResults.value = false;
|
||||||
activeLineIndex.value = null;
|
activeLineIndex.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close dropdowns on click outside
|
// Close dropdowns on click outside
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const supplierContainer = document.querySelector('.supplier-search-container');
|
const supplierContainer = document.querySelector(
|
||||||
|
".supplier-search-container"
|
||||||
|
);
|
||||||
if (supplierContainer && !supplierContainer.contains(event.target)) {
|
if (supplierContainer && !supplierContainer.contains(event.target)) {
|
||||||
showSupplierResults.value = false;
|
showSupplierResults.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const productContainers = document.querySelectorAll('.product-search-container');
|
const productContainers = document.querySelectorAll(
|
||||||
|
".product-search-container"
|
||||||
|
);
|
||||||
let clickedInsideAnyProduct = false;
|
let clickedInsideAnyProduct = false;
|
||||||
productContainers.forEach(container => {
|
productContainers.forEach((container) => {
|
||||||
if (container.contains(event.target)) {
|
if (container.contains(event.target)) {
|
||||||
clickedInsideAnyProduct = true;
|
clickedInsideAnyProduct = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!clickedInsideAnyProduct) {
|
if (!clickedInsideAnyProduct) {
|
||||||
showProductResults.value = false;
|
showProductResults.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside);
|
document.removeEventListener("click", handleClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
@ -493,7 +529,10 @@ const removeLine = (index) => {
|
|||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
if (!formData.value.supplierId) {
|
if (!formData.value.supplierId) {
|
||||||
notificationStore.error("Champ requis", "Veuillez sélectionner un fournisseur.");
|
notificationStore.error(
|
||||||
|
"Champ requis",
|
||||||
|
"Veuillez sélectionner un fournisseur."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,26 +547,27 @@ const submitForm = async () => {
|
|||||||
total_ht: calculateTotalHt(),
|
total_ht: calculateTotalHt(),
|
||||||
total_tva: calculateTotalTva(),
|
total_tva: calculateTotalTva(),
|
||||||
total_ttc: calculateTotalTtc(),
|
total_ttc: calculateTotalTtc(),
|
||||||
lines: formData.value.lines.map(line => ({
|
lines: formData.value.lines.map((line) => ({
|
||||||
product_id: line.productId || null,
|
product_id: line.productId || null,
|
||||||
description: line.designation,
|
description: line.designation,
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
unit_price: line.priceHt,
|
unit_price: line.priceHt,
|
||||||
tva_rate: 20, // Default 20%
|
tva_rate: 20, // Default 20%
|
||||||
total_ht: line.quantity * line.priceHt
|
total_ht: line.quantity * line.priceHt,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Submitting purchase order:", payload);
|
console.log("Submitting purchase order:", payload);
|
||||||
const response = await PurchaseOrderService.createPurchaseOrder(payload);
|
const response = await PurchaseOrderService.createPurchaseOrder(payload);
|
||||||
console.log("Purchase order created:", response);
|
console.log("Purchase order created:", response);
|
||||||
|
|
||||||
emit("submit", response.data);
|
emit("submit", response.data);
|
||||||
notificationStore.success("Succès", "Commande créée avec succès !");
|
notificationStore.success("Succès", "Commande créée avec succès !");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating purchase order:", error);
|
console.error("Error creating purchase order:", error);
|
||||||
const message = error.response?.data?.message || "Une erreur est survenue lors de la création de la commande.";
|
const message =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
"Une erreur est survenue lors de la création de la commande.";
|
||||||
notificationStore.error("Erreur", message);
|
notificationStore.error("Erreur", message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -862,21 +902,21 @@ const cancelForm = () => {
|
|||||||
.form-section {
|
.form-section {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals-content {
|
.totals-content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cancel,
|
.btn-cancel,
|
||||||
.btn-submit {
|
.btn-submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -1,53 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="submitForm" class="reception-form">
|
<form class="reception-form" @submit.prevent="submitForm">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<i class="fas fa-file-invoice"></i>
|
<i class="fas fa-file-invoice"></i>
|
||||||
Informations générales
|
Informations générales
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 1: Commande Fournisseur, Entrepôt -->
|
<!-- Row 1: Commande Fournisseur, Entrepôt -->
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Commande Fournisseur <span class="text-danger">*</span></label>
|
<label class="form-label"
|
||||||
<select
|
>Commande Fournisseur <span class="text-danger">*</span></label
|
||||||
v-model="formData.purchase_order_id"
|
>
|
||||||
|
<select
|
||||||
|
v-model="formData.purchase_order_id"
|
||||||
class="form-select custom-select"
|
class="form-select custom-select"
|
||||||
:class="{ 'is-invalid': errors.purchase_order_id }"
|
:class="{ 'is-invalid': errors.purchase_order_id }"
|
||||||
>
|
>
|
||||||
<option value="">Sélectionner une commande</option>
|
<option value="">Sélectionner une commande</option>
|
||||||
<option v-for="po in purchaseOrders" :key="po.id" :value="po.id">
|
<option v-for="po in purchaseOrders" :key="po.id" :value="po.id">
|
||||||
{{ po.po_number }} - {{ po.fournisseur?.nom || 'Fournisseur ' + po.fournisseur_id }}
|
{{ po.po_number }} -
|
||||||
|
{{ po.fournisseur?.nom || "Fournisseur " + po.fournisseur_id }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div v-if="errors.purchase_order_id" class="invalid-feedback">
|
<div v-if="errors.purchase_order_id" class="invalid-feedback">
|
||||||
{{ errors.purchase_order_id }}
|
{{ errors.purchase_order_id }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 position-relative warehouse-search-container">
|
<div class="col-md-6 position-relative warehouse-search-container">
|
||||||
<label class="form-label">Entrepôt de Destination <span class="text-danger">*</span></label>
|
<label class="form-label"
|
||||||
|
>Entrepôt de Destination <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model="warehouseSearchQuery"
|
v-model="warehouseSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher un entrepôt..."
|
placeholder="Rechercher un entrepôt..."
|
||||||
@input="handleWarehouseSearch"
|
|
||||||
@focus="showWarehouseResults = true"
|
|
||||||
required
|
required
|
||||||
class="search-input"
|
class="search-input"
|
||||||
|
@input="handleWarehouseSearch"
|
||||||
|
@focus="showWarehouseResults = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Results Dropdown -->
|
<!-- Search Results Dropdown -->
|
||||||
<div
|
<div
|
||||||
v-if="showWarehouseResults && (warehouseSearchResults.length > 0 || isSearchingWarehouses)"
|
v-if="
|
||||||
|
showWarehouseResults &&
|
||||||
|
(warehouseSearchResults.length > 0 || isSearchingWarehouses)
|
||||||
|
"
|
||||||
class="search-dropdown"
|
class="search-dropdown"
|
||||||
>
|
>
|
||||||
<div v-if="isSearchingWarehouses" class="dropdown-loading">
|
<div v-if="isSearchingWarehouses" class="dropdown-loading">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +72,8 @@
|
|||||||
>
|
>
|
||||||
<span class="item-name">{{ warehouse.name }}</span>
|
<span class="item-name">{{ warehouse.name }}</span>
|
||||||
<span class="item-details">
|
<span class="item-details">
|
||||||
{{ warehouse.city || 'Ville non spécifiée' }} • {{ warehouse.country_code || '' }}
|
{{ warehouse.city || "Ville non spécifiée" }} •
|
||||||
|
{{ warehouse.country_code || "" }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@ -83,12 +95,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Date de Réception <span class="text-danger">*</span></label>
|
<label class="form-label"
|
||||||
<soft-input
|
>Date de Réception <span class="text-danger">*</span></label
|
||||||
v-model="formData.receipt_date"
|
>
|
||||||
type="date"
|
<soft-input v-model="formData.receipt_date" type="date" required />
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Statut</label>
|
<label class="form-label">Statut</label>
|
||||||
@ -125,8 +135,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="addLine"
|
|
||||||
class="add-btn"
|
class="add-btn"
|
||||||
|
@click="addLine"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i> Ajouter ligne
|
<i class="fas fa-plus"></i> Ajouter ligne
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -140,31 +150,42 @@
|
|||||||
>
|
>
|
||||||
<div class="row g-2 align-items-end">
|
<div class="row g-2 align-items-end">
|
||||||
<div class="col-md-3 position-relative product-search-container">
|
<div class="col-md-3 position-relative product-search-container">
|
||||||
<label class="form-label text-xs">Produit <span class="text-danger">*</span></label>
|
<label class="form-label text-xs"
|
||||||
|
>Produit <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<soft-input
|
<soft-input
|
||||||
v-model="line.searchQuery"
|
v-model="line.searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher un produit..."
|
placeholder="Rechercher un produit..."
|
||||||
@input="handleProductSearch(index)"
|
|
||||||
@focus="activeLineIndex = index; showProductResults = true"
|
|
||||||
required
|
required
|
||||||
class="search-input"
|
class="search-input"
|
||||||
|
@input="handleProductSearch(index)"
|
||||||
|
@focus="
|
||||||
|
activeLineIndex = index;
|
||||||
|
showProductResults = true;
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Search Results Dropdown -->
|
<!-- Product Search Results Dropdown -->
|
||||||
<div
|
<div
|
||||||
v-show="showProductResults && activeLineIndex === index"
|
v-show="showProductResults && activeLineIndex === index"
|
||||||
class="search-dropdown product-dropdown"
|
class="search-dropdown product-dropdown"
|
||||||
>
|
>
|
||||||
<div v-if="isSearchingProducts" class="dropdown-loading">
|
<div v-if="isSearchingProducts" class="dropdown-loading">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
<span class="visually-hidden">Chargement...</span>
|
<span class="visually-hidden">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="productSearchResults.length === 0" class="dropdown-empty">
|
<div
|
||||||
|
v-else-if="productSearchResults.length === 0"
|
||||||
|
class="dropdown-empty"
|
||||||
|
>
|
||||||
Aucun produit trouvé
|
Aucun produit trouvé
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -177,26 +198,31 @@
|
|||||||
>
|
>
|
||||||
<span class="item-name">{{ product.nom }}</span>
|
<span class="item-name">{{ product.nom }}</span>
|
||||||
<span class="item-details">
|
<span class="item-details">
|
||||||
Réf: {{ product.reference }} • Stock: {{ product.stock_actuel }} {{ product.unite }}
|
Réf: {{ product.reference }} • Stock:
|
||||||
|
{{ product.stock_actuel }} {{ product.unite }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Conditionnement</label>
|
<label class="form-label text-xs">Conditionnement</label>
|
||||||
<select
|
<select
|
||||||
class="form-select form-select-sm"
|
|
||||||
v-model="line.packaging_id"
|
v-model="line.packaging_id"
|
||||||
|
class="form-select form-select-sm"
|
||||||
>
|
>
|
||||||
<option :value="null">Unité</option>
|
<option :value="null">Unité</option>
|
||||||
<option v-for="pkg in getPackagings(line.product_id)" :key="pkg.id" :value="pkg.id">
|
<option
|
||||||
|
v-for="pkg in getPackagings(line.product_id)"
|
||||||
|
:key="pkg.id"
|
||||||
|
:value="pkg.id"
|
||||||
|
>
|
||||||
{{ pkg.name }} ({{ pkg.qty_base }} unités)
|
{{ pkg.name }} ({{ pkg.qty_base }} unités)
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Colis Reçus</label>
|
<label class="form-label text-xs">Colis Reçus</label>
|
||||||
<soft-input
|
<soft-input
|
||||||
@ -207,7 +233,7 @@
|
|||||||
step="0.001"
|
step="0.001"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Unités Reçues</label>
|
<label class="form-label text-xs">Unités Reçues</label>
|
||||||
<soft-input
|
<soft-input
|
||||||
@ -218,7 +244,7 @@
|
|||||||
step="0.001"
|
step="0.001"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Prix Unitaire</label>
|
<label class="form-label text-xs">Prix Unitaire</label>
|
||||||
<soft-input
|
<soft-input
|
||||||
@ -229,14 +255,14 @@
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-1 d-flex flex-column align-items-end gap-2">
|
<div class="col-md-1 d-flex flex-column align-items-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-delete"
|
class="btn-delete"
|
||||||
@click="removeLine(index)"
|
|
||||||
:disabled="formData.lines.length === 1"
|
:disabled="formData.lines.length === 1"
|
||||||
title="Supprimer la ligne"
|
title="Supprimer la ligne"
|
||||||
|
@click="removeLine(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -252,8 +278,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="cancelForm"
|
|
||||||
class="btn-cancel"
|
class="btn-cancel"
|
||||||
|
@click="cancelForm"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i> Annuler
|
<i class="fas fa-times"></i> Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -270,11 +296,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, defineProps, onMounted, onUnmounted } from "vue";
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
defineEmits,
|
||||||
|
defineProps,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
} from "vue";
|
||||||
import SoftInput from "@/components/SoftInput.vue";
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import WarehouseService from "@/services/warehouse";
|
import WarehouseService from "@/services/warehouse";
|
||||||
import ProductService from "@/services/product";
|
import ProductService from "@/services/product";
|
||||||
|
import { PurchaseOrderService } from "@/services/purchaseOrder";
|
||||||
import { useNotificationStore } from "@/stores/notification";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -315,8 +349,10 @@ const handleWarehouseSearch = () => {
|
|||||||
isSearchingWarehouses.value = true;
|
isSearchingWarehouses.value = true;
|
||||||
showWarehouseResults.value = true;
|
showWarehouseResults.value = true;
|
||||||
try {
|
try {
|
||||||
const results = await WarehouseService.searchWarehouses(warehouseSearchQuery.value);
|
const results = await WarehouseService.searchWarehouses(
|
||||||
warehouseSearchResults.value = results.filter(w => w && w.id);
|
warehouseSearchQuery.value
|
||||||
|
);
|
||||||
|
warehouseSearchResults.value = results.filter((w) => w && w.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error searching warehouses:", error);
|
console.error("Error searching warehouses:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -328,7 +364,7 @@ const handleWarehouseSearch = () => {
|
|||||||
const selectWarehouse = (warehouse) => {
|
const selectWarehouse = (warehouse) => {
|
||||||
if (!warehouse || !warehouse.id) return;
|
if (!warehouse || !warehouse.id) return;
|
||||||
if (searchTimeout) clearTimeout(searchTimeout);
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
|
|
||||||
formData.value.warehouse_id = warehouse.id;
|
formData.value.warehouse_id = warehouse.id;
|
||||||
warehouseSearchQuery.value = warehouse.name;
|
warehouseSearchQuery.value = warehouse.name;
|
||||||
showWarehouseResults.value = false;
|
showWarehouseResults.value = false;
|
||||||
@ -361,10 +397,11 @@ const handleProductSearch = (index) => {
|
|||||||
showProductResults.value = true;
|
showProductResults.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await ProductService.searchProducts(query);
|
const response = await ProductService.searchProducts(query);
|
||||||
if (activeLineIndex.value === index &&
|
if (
|
||||||
formData.value.lines[index] &&
|
activeLineIndex.value === index &&
|
||||||
formData.value.lines[index].searchQuery === query) {
|
formData.value.lines[index] &&
|
||||||
|
formData.value.lines[index].searchQuery === query
|
||||||
|
) {
|
||||||
let results = [];
|
let results = [];
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
@ -373,8 +410,8 @@ const handleProductSearch = (index) => {
|
|||||||
results = response.data.data;
|
results = response.data.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
productSearchResults.value = results.filter(p => p && p.id);
|
productSearchResults.value = results.filter((p) => p && p.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error searching products:", error);
|
console.error("Error searching products:", error);
|
||||||
@ -392,13 +429,13 @@ const selectProduct = (index, product) => {
|
|||||||
|
|
||||||
const line = formData.value.lines[index];
|
const line = formData.value.lines[index];
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
|
|
||||||
line.product_id = product.id;
|
line.product_id = product.id;
|
||||||
line.searchQuery = product.nom;
|
line.searchQuery = product.nom;
|
||||||
|
|
||||||
// Load packagings for this product
|
// Load packagings for this product
|
||||||
loadProductPackagings(product.id);
|
loadProductPackagings(product.id);
|
||||||
|
|
||||||
showProductResults.value = false;
|
showProductResults.value = false;
|
||||||
activeLineIndex.value = null;
|
activeLineIndex.value = null;
|
||||||
};
|
};
|
||||||
@ -408,11 +445,15 @@ const loadProductPackagings = async (productId) => {
|
|||||||
const response = await ProductService.getProduct(productId);
|
const response = await ProductService.getProduct(productId);
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
// Store packagings for this product
|
// Store packagings for this product
|
||||||
productPackagings.value[productId] = response.data.conditionnement ? [{
|
productPackagings.value[productId] = response.data.conditionnement
|
||||||
id: 1,
|
? [
|
||||||
name: response.data.conditionnement_nom || 'Conditionnement',
|
{
|
||||||
qty_base: response.data.conditionnement_quantite || 1
|
id: 1,
|
||||||
}] : [];
|
name: response.data.conditionnement_nom || "Conditionnement",
|
||||||
|
qty_base: response.data.conditionnement_quantite || 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading product packagings:", error);
|
console.error("Error loading product packagings:", error);
|
||||||
@ -425,30 +466,34 @@ const getPackagings = (productId) => {
|
|||||||
|
|
||||||
// Close dropdowns on click outside
|
// Close dropdowns on click outside
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const warehouseContainer = document.querySelector('.warehouse-search-container');
|
const warehouseContainer = document.querySelector(
|
||||||
|
".warehouse-search-container"
|
||||||
|
);
|
||||||
if (warehouseContainer && !warehouseContainer.contains(event.target)) {
|
if (warehouseContainer && !warehouseContainer.contains(event.target)) {
|
||||||
showWarehouseResults.value = false;
|
showWarehouseResults.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const productContainers = document.querySelectorAll('.product-search-container');
|
const productContainers = document.querySelectorAll(
|
||||||
|
".product-search-container"
|
||||||
|
);
|
||||||
let clickedInsideAnyProduct = false;
|
let clickedInsideAnyProduct = false;
|
||||||
productContainers.forEach(container => {
|
productContainers.forEach((container) => {
|
||||||
if (container.contains(event.target)) {
|
if (container.contains(event.target)) {
|
||||||
clickedInsideAnyProduct = true;
|
clickedInsideAnyProduct = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!clickedInsideAnyProduct) {
|
if (!clickedInsideAnyProduct) {
|
||||||
showProductResults.value = false;
|
showProductResults.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside);
|
document.removeEventListener("click", handleClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
@ -472,6 +517,60 @@ const formData = ref({
|
|||||||
|
|
||||||
const errors = ref({});
|
const errors = ref({});
|
||||||
|
|
||||||
|
const hydrateLinesFromPurchaseOrder = async (purchaseOrderId) => {
|
||||||
|
if (!purchaseOrderId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await PurchaseOrderService.getPurchaseOrder(
|
||||||
|
parseInt(purchaseOrderId)
|
||||||
|
);
|
||||||
|
const order = response?.data;
|
||||||
|
const orderLines = Array.isArray(order?.lines)
|
||||||
|
? order.lines
|
||||||
|
: Array.isArray(order?.lines?.data)
|
||||||
|
? order.lines.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (orderLines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.value.lines = orderLines.map((line) => ({
|
||||||
|
product_id: line.product_id || "",
|
||||||
|
searchQuery: line.product?.nom || line.description || "",
|
||||||
|
packaging_id: null,
|
||||||
|
packages_qty_received: null,
|
||||||
|
units_qty_received: Number(line.quantity || 0) || null,
|
||||||
|
unit_price: Number(line.unit_price || 0) || null,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error hydrating receipt lines from purchase order:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formData.value.purchase_order_id,
|
||||||
|
async (newPurchaseOrderId) => {
|
||||||
|
if (!newPurchaseOrderId) {
|
||||||
|
formData.value.lines = [
|
||||||
|
{
|
||||||
|
product_id: "",
|
||||||
|
searchQuery: "",
|
||||||
|
packaging_id: null,
|
||||||
|
packages_qty_received: null,
|
||||||
|
units_qty_received: null,
|
||||||
|
unit_price: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hydrateLinesFromPurchaseOrder(newPurchaseOrderId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const addLine = () => {
|
const addLine = () => {
|
||||||
formData.value.lines.push({
|
formData.value.lines.push({
|
||||||
product_id: "",
|
product_id: "",
|
||||||
@ -515,13 +614,15 @@ const submitForm = () => {
|
|||||||
receipt_date: formData.value.receipt_date,
|
receipt_date: formData.value.receipt_date,
|
||||||
status: formData.value.status,
|
status: formData.value.status,
|
||||||
notes: formData.value.notes || undefined,
|
notes: formData.value.notes || undefined,
|
||||||
lines: formData.value.lines.map(line => ({
|
lines: formData.value.lines
|
||||||
product_id: parseInt(line.product_id),
|
.map((line) => ({
|
||||||
packaging_id: line.packaging_id ? parseInt(line.packaging_id) : null,
|
product_id: parseInt(line.product_id),
|
||||||
packages_qty_received: line.packages_qty_received,
|
packaging_id: line.packaging_id ? parseInt(line.packaging_id) : null,
|
||||||
units_qty_received: line.units_qty_received,
|
packages_qty_received: line.packages_qty_received,
|
||||||
unit_price: line.unit_price,
|
units_qty_received: line.units_qty_received,
|
||||||
})).filter(line => line.product_id),
|
unit_price: line.unit_price,
|
||||||
|
}))
|
||||||
|
.filter((line) => line.product_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
emit("submit", payload);
|
emit("submit", payload);
|
||||||
|
|||||||
@ -1,22 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li class="list-group-item border-0 ps-0 pt-0 text-sm">
|
<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 }}
|
<strong class="text-dark">Nom de l'entrepôt:</strong>
|
||||||
|
{{ warehouse.name }}
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item border-0 ps-0 text-sm">
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
<strong class="text-dark">Adresse 1:</strong> {{ warehouse.address_line1 || '-' }}
|
<strong class="text-dark">Adresse 1:</strong>
|
||||||
|
{{ warehouse.address_line1 || "-" }}
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item border-0 ps-0 text-sm">
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
<strong class="text-dark">Adresse 2:</strong> {{ warehouse.address_line2 || '-' }}
|
<strong class="text-dark">Adresse 2:</strong>
|
||||||
|
{{ warehouse.address_line2 || "-" }}
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item border-0 ps-0 text-sm">
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
<strong class="text-dark">Code Postal:</strong> {{ warehouse.postal_code || '-' }}
|
<strong class="text-dark">Code Postal:</strong>
|
||||||
|
{{ warehouse.postal_code || "-" }}
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item border-0 ps-0 text-sm">
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
<strong class="text-dark">Ville:</strong> {{ warehouse.city || '-' }}
|
<strong class="text-dark">Ville:</strong>
|
||||||
|
{{ warehouse.city || "-" }}
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item border-0 ps-0 text-sm">
|
<li class="list-group-item border-0 ps-0 text-sm">
|
||||||
<strong class="text-dark">Pays:</strong> {{ warehouse.country_code }}
|
<strong class="text-dark">Pays:</strong>
|
||||||
|
{{ warehouse.country_code }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -58,7 +58,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="alert alert-danger text-white text-sm mt-3" role="alert">
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="alert alert-danger text-white text-sm mt-3"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -67,8 +71,8 @@
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
class="me-2"
|
class="me-2"
|
||||||
@click="$emit('cancel')"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
|
|||||||
@ -42,7 +42,9 @@
|
|||||||
alt="supplier image"
|
alt="supplier image"
|
||||||
circular
|
circular
|
||||||
/>
|
/>
|
||||||
<span>{{ commande.fournisseur?.name || commande.supplier }}</span>
|
<span>{{
|
||||||
|
commande.fournisseur?.name || commande.supplier
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -72,7 +74,9 @@
|
|||||||
|
|
||||||
<!-- Items Count -->
|
<!-- Items Count -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="badge bg-secondary">{{ commande.lines?.length || commande.items_count || 0 }}</span>
|
<span class="badge bg-secondary">{{
|
||||||
|
commande.lines?.length || commande.items_count || 0
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
|
|||||||
@ -21,24 +21,33 @@
|
|||||||
|
|
||||||
<!-- Receipt Date -->
|
<!-- Receipt Date -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{ formatDate(receipt.receipt_date) }}</span>
|
<span class="my-2 text-xs">{{
|
||||||
|
formatDate(receipt.receipt_date)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Purchase Order -->
|
<!-- Purchase Order -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">
|
<span class="my-2 text-xs">
|
||||||
{{ receipt.purchase_order?.po_number || receipt.purchase_order_id }}
|
{{
|
||||||
|
receipt.purchase_order?.po_number || receipt.purchase_order_id
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Warehouse -->
|
<!-- Warehouse -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{ receipt.warehouse?.name || '-' }}</span>
|
<span class="my-2 text-xs">{{
|
||||||
|
receipt.warehouse?.name || "-"
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<soft-badge :color="getStatusColor(receipt.status)" variant="gradient">
|
<soft-badge
|
||||||
|
:color="getStatusColor(receipt.status)"
|
||||||
|
variant="gradient"
|
||||||
|
>
|
||||||
{{ getStatusLabel(receipt.status) }}
|
{{ getStatusLabel(receipt.status) }}
|
||||||
</soft-badge>
|
</soft-badge>
|
||||||
</td>
|
</td>
|
||||||
@ -107,28 +116,28 @@ const props = defineProps({
|
|||||||
const dataTableInstance = ref(null);
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return '-';
|
if (!dateString) return "-";
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('fr-FR');
|
return date.toLocaleDateString("fr-FR");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
const getStatusColor = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'draft':
|
case "draft":
|
||||||
return 'secondary';
|
return "secondary";
|
||||||
case 'posted':
|
case "posted":
|
||||||
return 'success';
|
return "success";
|
||||||
default:
|
default:
|
||||||
return 'info';
|
return "info";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status) => {
|
const getStatusLabel = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'draft':
|
case "draft":
|
||||||
return 'Brouillon';
|
return "Brouillon";
|
||||||
case 'posted':
|
case "posted":
|
||||||
return 'Validée';
|
return "Validée";
|
||||||
default:
|
default:
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,19 +25,25 @@
|
|||||||
|
|
||||||
<!-- City -->
|
<!-- City -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{ warehouse.city || '-' }}</span>
|
<span class="my-2 text-xs">{{ warehouse.city || "-" }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Postal Code -->
|
<!-- Postal Code -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{ warehouse.postal_code || '-' }}</span>
|
<span class="my-2 text-xs">{{
|
||||||
|
warehouse.postal_code || "-"
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Address -->
|
<!-- Address -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">
|
<span class="my-2 text-xs">
|
||||||
{{ warehouse.address_line1 }}
|
{{ warehouse.address_line1 }}
|
||||||
<span v-if="warehouse.address_line2" class="d-block text-secondary">{{ warehouse.address_line2 }}</span>
|
<span
|
||||||
|
v-if="warehouse.address_line2"
|
||||||
|
class="d-block text-secondary"
|
||||||
|
>{{ warehouse.address_line2 }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export interface GoodsReceipt {
|
|||||||
warehouse_id: number;
|
warehouse_id: number;
|
||||||
receipt_number: string;
|
receipt_number: string;
|
||||||
receipt_date: string;
|
receipt_date: string;
|
||||||
status: 'draft' | 'posted';
|
status: "draft" | "posted";
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@ -86,7 +86,8 @@ export interface CreateGoodsReceiptPayload {
|
|||||||
lines?: CreateGoodsReceiptLinePayload[];
|
lines?: CreateGoodsReceiptLinePayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateGoodsReceiptPayload extends Partial<CreateGoodsReceiptPayload> {
|
export interface UpdateGoodsReceiptPayload
|
||||||
|
extends Partial<CreateGoodsReceiptPayload> {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +115,9 @@ export const GoodsReceiptService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createGoodsReceipt(payload: CreateGoodsReceiptPayload): Promise<GoodsReceiptResponse> {
|
async createGoodsReceipt(
|
||||||
|
payload: CreateGoodsReceiptPayload
|
||||||
|
): Promise<GoodsReceiptResponse> {
|
||||||
const response = await request<GoodsReceiptResponse>({
|
const response = await request<GoodsReceiptResponse>({
|
||||||
url: "/api/goods-receipts",
|
url: "/api/goods-receipts",
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -123,7 +126,9 @@ export const GoodsReceiptService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateGoodsReceipt(payload: UpdateGoodsReceiptPayload): Promise<GoodsReceiptResponse> {
|
async updateGoodsReceipt(
|
||||||
|
payload: UpdateGoodsReceiptPayload
|
||||||
|
): Promise<GoodsReceiptResponse> {
|
||||||
const { id, ...updateData } = payload;
|
const { id, ...updateData } = payload;
|
||||||
const response = await request<GoodsReceiptResponse>({
|
const response = await request<GoodsReceiptResponse>({
|
||||||
url: `/api/goods-receipts/${id}`,
|
url: `/api/goods-receipts/${id}`,
|
||||||
@ -133,7 +138,9 @@ export const GoodsReceiptService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteGoodsReceipt(id: number): Promise<{ success: boolean; message: string }> {
|
async deleteGoodsReceipt(
|
||||||
|
id: number
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
const response = await request<{ success: boolean; message: string }>({
|
const response = await request<{ success: boolean; message: string }>({
|
||||||
url: `/api/goods-receipts/${id}`,
|
url: `/api/goods-receipts/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
|
|||||||
@ -32,7 +32,10 @@ class ProductPackagingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePackaging(id: number, data: any): Promise<{ data: ProductPackaging }> {
|
async updatePackaging(
|
||||||
|
id: number,
|
||||||
|
data: any
|
||||||
|
): Promise<{ data: ProductPackaging }> {
|
||||||
return await request<{ data: ProductPackaging }>({
|
return await request<{ data: ProductPackaging }>({
|
||||||
url: `/api/product-packagings/${id}`,
|
url: `/api/product-packagings/${id}`,
|
||||||
method: "put",
|
method: "put",
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export interface PurchaseOrder {
|
|||||||
id: number;
|
id: number;
|
||||||
fournisseur_id: number;
|
fournisseur_id: number;
|
||||||
po_number: string;
|
po_number: string;
|
||||||
status: 'brouillon' | 'confirmee' | 'livree' | 'facturee' | 'annulee';
|
status: "brouillon" | "confirmee" | "livree" | "facturee" | "annulee";
|
||||||
order_date: string;
|
order_date: string;
|
||||||
expected_date: string | null;
|
expected_date: string | null;
|
||||||
currency: string;
|
currency: string;
|
||||||
@ -69,7 +69,8 @@ export interface CreatePurchaseOrderPayload {
|
|||||||
lines?: CreatePurchaseOrderLinePayload[];
|
lines?: CreatePurchaseOrderLinePayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdatePurchaseOrderPayload extends Partial<CreatePurchaseOrderPayload> {
|
export interface UpdatePurchaseOrderPayload
|
||||||
|
extends Partial<CreatePurchaseOrderPayload> {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +98,9 @@ export const PurchaseOrderService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createPurchaseOrder(payload: CreatePurchaseOrderPayload): Promise<PurchaseOrderResponse> {
|
async createPurchaseOrder(
|
||||||
|
payload: CreatePurchaseOrderPayload
|
||||||
|
): Promise<PurchaseOrderResponse> {
|
||||||
const response = await request<PurchaseOrderResponse>({
|
const response = await request<PurchaseOrderResponse>({
|
||||||
url: "/api/purchase-orders",
|
url: "/api/purchase-orders",
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -106,7 +109,9 @@ export const PurchaseOrderService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updatePurchaseOrder(payload: UpdatePurchaseOrderPayload): Promise<PurchaseOrderResponse> {
|
async updatePurchaseOrder(
|
||||||
|
payload: UpdatePurchaseOrderPayload
|
||||||
|
): Promise<PurchaseOrderResponse> {
|
||||||
const { id, ...updateData } = payload;
|
const { id, ...updateData } = payload;
|
||||||
const response = await request<PurchaseOrderResponse>({
|
const response = await request<PurchaseOrderResponse>({
|
||||||
url: `/api/purchase-orders/${id}`,
|
url: `/api/purchase-orders/${id}`,
|
||||||
@ -116,7 +121,9 @@ export const PurchaseOrderService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deletePurchaseOrder(id: number): Promise<{ success: boolean; message: string }> {
|
async deletePurchaseOrder(
|
||||||
|
id: number
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
const response = await request<{ success: boolean; message: string }>({
|
const response = await request<{ success: boolean; message: string }>({
|
||||||
url: `/api/purchase-orders/${id}`,
|
url: `/api/purchase-orders/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
@ -124,7 +131,9 @@ export const PurchaseOrderService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getByFournisseur(fournisseurId: number): Promise<PurchaseOrderListResponse> {
|
async getByFournisseur(
|
||||||
|
fournisseurId: number
|
||||||
|
): Promise<PurchaseOrderListResponse> {
|
||||||
const response = await request<PurchaseOrderListResponse>({
|
const response = await request<PurchaseOrderListResponse>({
|
||||||
url: `/api/fournisseurs/${fournisseurId}/purchase-orders`,
|
url: `/api/fournisseurs/${fournisseurId}/purchase-orders`,
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|||||||
38
thanasoft-front/src/services/receipt.ts
Normal file
38
thanasoft-front/src/services/receipt.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
import type { GoodsReceipt } from "./goodsReceipt";
|
||||||
|
|
||||||
|
export interface ReceiptListResponse {
|
||||||
|
data: GoodsReceipt[] | { data: GoodsReceipt[] };
|
||||||
|
meta?: {
|
||||||
|
current_page: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReceiptService = {
|
||||||
|
async getReceipts(params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
status?: string;
|
||||||
|
}): Promise<ReceiptListResponse> {
|
||||||
|
return request<ReceiptListResponse>({
|
||||||
|
url: "/api/goods-receipts",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteReceipt(
|
||||||
|
id: number
|
||||||
|
): Promise<{ success?: boolean; message: string }> {
|
||||||
|
return request<{ success?: boolean; message: string }>({
|
||||||
|
url: `/api/goods-receipts/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReceiptService;
|
||||||
@ -44,7 +44,10 @@ class WarehouseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWarehouse(id: number, data: WarehouseFormData): Promise<{ data: Warehouse }> {
|
async updateWarehouse(
|
||||||
|
id: number,
|
||||||
|
data: WarehouseFormData
|
||||||
|
): Promise<{ data: Warehouse }> {
|
||||||
return await request<{ data: Warehouse }>({
|
return await request<{ data: Warehouse }>({
|
||||||
url: `/api/warehouses/${id}`,
|
url: `/api/warehouses/${id}`,
|
||||||
method: "put",
|
method: "put",
|
||||||
|
|||||||
@ -211,7 +211,9 @@ export const usePurchaseOrderStore = defineStore("purchaseOrder", () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await PurchaseOrderService.getByFournisseur(fournisseurId);
|
const response = await PurchaseOrderService.getByFournisseur(
|
||||||
|
fournisseurId
|
||||||
|
);
|
||||||
setPurchaseOrders(response.data);
|
setPurchaseOrders(response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
79
thanasoft-front/src/stores/receiptStore.ts
Normal file
79
thanasoft-front/src/stores/receiptStore.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import ReceiptService from "@/services/receipt";
|
||||||
|
import type { GoodsReceipt } from "@/services/goodsReceipt";
|
||||||
|
|
||||||
|
export const useReceiptStore = defineStore("receipt", () => {
|
||||||
|
const receipts = ref<GoodsReceipt[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const setLoading = (value: boolean) => {
|
||||||
|
loading.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (value: string | null) => {
|
||||||
|
error.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setReceipts = (items: GoodsReceipt[]) => {
|
||||||
|
receipts.value = items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchReceipts = async (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
status?: string;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await ReceiptService.getReceipts(params);
|
||||||
|
const normalized = Array.isArray(response.data)
|
||||||
|
? response.data
|
||||||
|
: response.data?.data || [];
|
||||||
|
setReceipts(normalized);
|
||||||
|
return normalized;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err?.response?.data?.message ||
|
||||||
|
err?.message ||
|
||||||
|
"Erreur lors du chargement des réceptions"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteReceipt = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await ReceiptService.deleteReceipt(id);
|
||||||
|
receipts.value = receipts.value.filter((r) => r.id !== id);
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err?.response?.data?.message ||
|
||||||
|
err?.message ||
|
||||||
|
"Erreur lors de la suppression de la réception"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
receipts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
hasError: computed(() => !!error.value),
|
||||||
|
fetchReceipts,
|
||||||
|
deleteReceipt,
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -55,7 +55,8 @@ export const useStockStore = defineStore("stock", {
|
|||||||
this.stockMoves = response.data;
|
this.stockMoves = response.data;
|
||||||
return this.stockMoves;
|
return this.stockMoves;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.error = error.message || "Erreur lors du chargement des mouvements de stock";
|
this.error =
|
||||||
|
error.message || "Erreur lors du chargement des mouvements de stock";
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@ -70,7 +71,9 @@ export const useStockStore = defineStore("stock", {
|
|||||||
this.stockMoves.unshift(response.data);
|
this.stockMoves.unshift(response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.error = error.message || "Erreur lors de l'enregistrement du mouvement de stock";
|
this.error =
|
||||||
|
error.message ||
|
||||||
|
"Erreur lors de l'enregistrement du mouvement de stock";
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import WarehouseService, { Warehouse, WarehouseFormData } from "@/services/warehouse";
|
import WarehouseService, {
|
||||||
|
Warehouse,
|
||||||
|
WarehouseFormData,
|
||||||
|
} from "@/services/warehouse";
|
||||||
|
|
||||||
const warehouseService = new WarehouseService();
|
const warehouseService = new WarehouseService();
|
||||||
|
|
||||||
@ -50,7 +53,8 @@ export const useWarehouseStore = defineStore("warehouse", {
|
|||||||
this.warehouses.push(response.data);
|
this.warehouses.push(response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.error = error.message || "Erreur lors de la création de l'entrepôt";
|
this.error =
|
||||||
|
error.message || "Erreur lors de la création de l'entrepôt";
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@ -71,7 +75,8 @@ export const useWarehouseStore = defineStore("warehouse", {
|
|||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.error = error.message || "Erreur lors de la mise à jour de l'entrepôt";
|
this.error =
|
||||||
|
error.message || "Erreur lors de la mise à jour de l'entrepôt";
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@ -88,7 +93,8 @@ export const useWarehouseStore = defineStore("warehouse", {
|
|||||||
this.currentWarehouse = null;
|
this.currentWarehouse = null;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.error = error.message || "Erreur lors de la suppression de l'entrepôt";
|
this.error =
|
||||||
|
error.message || "Erreur lors de la suppression de l'entrepôt";
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|||||||
@ -3,19 +3,31 @@
|
|||||||
<div class="d-sm-flex justify-content-between mb-4">
|
<div class="d-sm-flex justify-content-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="mb-0">Modifier la Réception de Marchandise</h5>
|
<h5 class="mb-0">Modifier la Réception de Marchandise</h5>
|
||||||
<p class="text-sm mb-0">Modifier les informations d'une réception existante.</p>
|
<p class="text-sm mb-0">
|
||||||
|
Modifier les informations d'une réception existante.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-sm-0 mt-3">
|
<div class="mt-sm-0 mt-3">
|
||||||
<soft-button color="secondary" variant="gradient" class="me-2" @click="handleCancel">
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="gradient"
|
||||||
|
class="me-2"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
<i class="fas fa-times me-2"></i> Annuler
|
<i class="fas fa-times me-2"></i> Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button color="info" variant="gradient" @click="handleSubmit" :loading="loading">
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="gradient"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
<i class="fas fa-save me-2"></i> Enregistrer
|
<i class="fas fa-save me-2"></i> Enregistrer
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="goodsReceipt">
|
<div v-if="goodsReceipt" class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -24,36 +36,56 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="purchase_order_id" class="form-control-label">Commande Fournisseur</label>
|
<label for="purchase_order_id" class="form-control-label"
|
||||||
<select
|
>Commande Fournisseur</label
|
||||||
id="purchase_order_id"
|
>
|
||||||
class="form-control"
|
<select
|
||||||
|
id="purchase_order_id"
|
||||||
v-model="form.purchase_order_id"
|
v-model="form.purchase_order_id"
|
||||||
|
class="form-control"
|
||||||
:class="{ 'is-invalid': errors.purchase_order_id }"
|
:class="{ 'is-invalid': errors.purchase_order_id }"
|
||||||
>
|
>
|
||||||
<option value="">Sélectionner une commande</option>
|
<option value="">Sélectionner une commande</option>
|
||||||
<option v-for="po in purchaseOrders" :key="po.id" :value="po.id">
|
<option
|
||||||
{{ po.po_number }} - {{ po.fournisseur?.nom || 'Fournisseur ' + po.fournisseur_id }}
|
v-for="po in purchaseOrders"
|
||||||
|
:key="po.id"
|
||||||
|
:value="po.id"
|
||||||
|
>
|
||||||
|
{{ po.po_number }} -
|
||||||
|
{{
|
||||||
|
po.fournisseur?.nom ||
|
||||||
|
"Fournisseur " + po.fournisseur_id
|
||||||
|
}}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">{{ errors.purchase_order_id }}</div>
|
<div class="invalid-feedback">
|
||||||
|
{{ errors.purchase_order_id }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="warehouse_id" class="form-control-label">Entrepôt de Destination</label>
|
<label for="warehouse_id" class="form-control-label"
|
||||||
<select
|
>Entrepôt de Destination</label
|
||||||
id="warehouse_id"
|
>
|
||||||
class="form-control"
|
<select
|
||||||
|
id="warehouse_id"
|
||||||
v-model="form.warehouse_id"
|
v-model="form.warehouse_id"
|
||||||
|
class="form-control"
|
||||||
:class="{ 'is-invalid': errors.warehouse_id }"
|
:class="{ 'is-invalid': errors.warehouse_id }"
|
||||||
>
|
>
|
||||||
<option value="">Sélectionner un entrepôt</option>
|
<option value="">Sélectionner un entrepôt</option>
|
||||||
<option v-for="wh in warehouses" :key="wh.id" :value="wh.id">
|
<option
|
||||||
|
v-for="wh in warehouses"
|
||||||
|
:key="wh.id"
|
||||||
|
:value="wh.id"
|
||||||
|
>
|
||||||
{{ wh.name }}
|
{{ wh.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="invalid-feedback">{{ errors.warehouse_id }}</div>
|
<div class="invalid-feedback">
|
||||||
|
{{ errors.warehouse_id }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -61,30 +93,40 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="receipt_number" class="form-control-label">Numéro de Réception</label>
|
<label for="receipt_number" class="form-control-label"
|
||||||
<input
|
>Numéro de Réception</label
|
||||||
id="receipt_number"
|
>
|
||||||
class="form-control"
|
<input
|
||||||
type="text"
|
id="receipt_number"
|
||||||
v-model="form.receipt_number"
|
v-model="form.receipt_number"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="receipt_date" class="form-control-label">Date de Réception</label>
|
<label for="receipt_date" class="form-control-label"
|
||||||
<input
|
>Date de Réception</label
|
||||||
id="receipt_date"
|
>
|
||||||
class="form-control"
|
<input
|
||||||
type="date"
|
id="receipt_date"
|
||||||
v-model="form.receipt_date"
|
v-model="form.receipt_date"
|
||||||
|
class="form-control"
|
||||||
|
type="date"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="status" class="form-control-label">Statut</label>
|
<label for="status" class="form-control-label"
|
||||||
<select id="status" class="form-control" v-model="form.status">
|
>Statut</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
v-model="form.status"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
<option value="draft">Brouillon</option>
|
<option value="draft">Brouillon</option>
|
||||||
<option value="posted">Validée</option>
|
<option value="posted">Validée</option>
|
||||||
</select>
|
</select>
|
||||||
@ -96,11 +138,11 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notes" class="form-control-label">Notes</label>
|
<label for="notes" class="form-control-label">Notes</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="notes"
|
id="notes"
|
||||||
class="form-control"
|
|
||||||
rows="3"
|
|
||||||
v-model="form.notes"
|
v-model="form.notes"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -133,7 +175,9 @@ const goodsReceiptStore = useGoodsReceiptStore();
|
|||||||
const warehouseStore = useWarehouseStore();
|
const warehouseStore = useWarehouseStore();
|
||||||
const purchaseOrderStore = usePurchaseOrderStore();
|
const purchaseOrderStore = usePurchaseOrderStore();
|
||||||
|
|
||||||
const { currentGoodsReceipt: goodsReceipt, loading } = storeToRefs(goodsReceiptStore);
|
const { currentGoodsReceipt: goodsReceipt, loading } = storeToRefs(
|
||||||
|
goodsReceiptStore
|
||||||
|
);
|
||||||
|
|
||||||
const warehouses = ref([]);
|
const warehouses = ref([]);
|
||||||
const purchaseOrders = ref([]);
|
const purchaseOrders = ref([]);
|
||||||
|
|||||||
@ -3,5 +3,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
import GoodsReceiptListPresentation from "@/components/Organism/Stock/GoodsReceiptListPresentation.vue";
|
import GoodsReceiptListPresentation from "@/components/Organism/Stock/GoodsReceiptListPresentation.vue";
|
||||||
|
import { useReceiptStore } from "@/stores/receiptStore";
|
||||||
|
|
||||||
|
const receiptStore = useReceiptStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
receiptStore.fetchReceipts();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user