Compare commits
5 Commits
094c7a0980
...
11750a3ffc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11750a3ffc | ||
|
|
dc87b0f720 | ||
|
|
ecfe25d3ca | ||
|
|
083f78673e | ||
|
|
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;
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
<div class="modal-body p-3 md:p-6">
|
<div class="modal-body p-3 md:p-6">
|
||||||
<form class="space-y-4 md:space-y-6" @submit.prevent="handleSubmit">
|
<form class="space-y-4 md:space-y-6" @submit.prevent="handleSubmit">
|
||||||
|
|
||||||
<!-- Global Errors -->
|
<!-- Global Errors -->
|
||||||
<div
|
<div
|
||||||
v-if="globalErrors.length"
|
v-if="globalErrors.length"
|
||||||
@ -43,14 +42,22 @@
|
|||||||
<!-- ROW 1: Défunt -->
|
<!-- ROW 1: Défunt -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div
|
||||||
<label class="form-label fw-bold small text-dark mb-0">Défunt *</label>
|
class="d-flex justify-content-between align-items-center mb-1"
|
||||||
|
>
|
||||||
|
<label class="form-label fw-bold small text-dark mb-0"
|
||||||
|
>Défunt *</label
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-primary py-0 px-2"
|
class="btn btn-sm btn-outline-primary py-0 px-2"
|
||||||
@click="toggleDeceasedMode"
|
@click="toggleDeceasedMode"
|
||||||
>
|
>
|
||||||
{{ deceasedForm.is_existing ? "+ Créer défunt" : "Rechercher défunt" }}
|
{{
|
||||||
|
deceasedForm.is_existing
|
||||||
|
? "+ Créer défunt"
|
||||||
|
: "Rechercher défunt"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -69,15 +76,27 @@
|
|||||||
@input="handleDeceasedSearch"
|
@input="handleDeceasedSearch"
|
||||||
@focus="showDeceasedResults = true"
|
@focus="showDeceasedResults = true"
|
||||||
/>
|
/>
|
||||||
<button v-if="deceasedForm.id" class="btn btn-outline-secondary" type="button" @click="clearDeceasedSelection">
|
<button
|
||||||
|
v-if="deceasedForm.id"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearDeceasedSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getFieldError('deceased_id')" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('deceased_id')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("deceased_id") }}
|
{{ getFieldError("deceased_id") }}
|
||||||
</div>
|
</div>
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div v-if="showDeceasedResults && deceasedSearchResults.length" class="list-group position-absolute w-100 shadow mt-1" style="z-index: 1050; max-height: 200px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showDeceasedResults && deceasedSearchResults.length"
|
||||||
|
class="list-group position-absolute w-100 shadow mt-1"
|
||||||
|
style="z-index: 1050; max-height: 200px; overflow-y: auto"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="d in deceasedSearchResults"
|
v-for="d in deceasedSearchResults"
|
||||||
:key="d.id"
|
:key="d.id"
|
||||||
@ -86,8 +105,12 @@
|
|||||||
@click="selectDeceased(d)"
|
@click="selectDeceased(d)"
|
||||||
>
|
>
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h6 class="mb-1 text-sm">{{ d.first_name }} {{ d.last_name }}</h6>
|
<h6 class="mb-1 text-sm">
|
||||||
<small class="text-xs text-muted">{{ d.birth_date }} - {{ d.death_date }}</small>
|
{{ d.first_name }} {{ d.last_name }}
|
||||||
|
</h6>
|
||||||
|
<small class="text-xs text-muted"
|
||||||
|
>{{ d.birth_date }} - {{ d.death_date }}</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -97,23 +120,53 @@
|
|||||||
<div v-else class="card card-body bg-light border-0 p-3">
|
<div v-else class="card card-body bg-light border-0 p-3">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small text-muted mb-1">Nom *</label>
|
<label class="form-label small text-muted mb-1"
|
||||||
<input v-model="deceasedForm.last_name" class="form-control form-control-sm" :class="{ 'is-invalid': hasError('deceased.last_name') }">
|
>Nom *</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="deceasedForm.last_name"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
:class="{
|
||||||
|
'is-invalid': hasError('deceased.last_name'),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small text-muted mb-1">Prénom</label>
|
<label class="form-label small text-muted mb-1"
|
||||||
<input v-model="deceasedForm.first_name" class="form-control form-control-sm">
|
>Prénom</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="deceasedForm.first_name"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small text-muted mb-1">Date Naissance</label>
|
<label class="form-label small text-muted mb-1"
|
||||||
<input v-model="deceasedForm.birth_date" type="date" class="form-control form-control-sm">
|
>Date Naissance</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="deceasedForm.birth_date"
|
||||||
|
type="date"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small text-muted mb-1">Date Décès</label>
|
<label class="form-label small text-muted mb-1"
|
||||||
<input v-model="deceasedForm.death_date" type="date" class="form-control form-control-sm">
|
>Date Décès</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="deceasedForm.death_date"
|
||||||
|
type="date"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12" v-if="getFieldError('deceased.last_name')">
|
<div
|
||||||
<small class="text-danger">{{ getFieldError('deceased.last_name') }}</small>
|
v-if="getFieldError('deceased.last_name')"
|
||||||
|
class="col-12"
|
||||||
|
>
|
||||||
|
<small class="text-danger">{{
|
||||||
|
getFieldError("deceased.last_name")
|
||||||
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -123,7 +176,9 @@
|
|||||||
<!-- ROW 2: Client -->
|
<!-- ROW 2: Client -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12 position-relative">
|
<div class="col-12 position-relative">
|
||||||
<label class="form-label fw-bold small text-dark">Client (Donneur d'ordre) *</label>
|
<label class="form-label fw-bold small text-dark"
|
||||||
|
>Client (Donneur d'ordre) *</label
|
||||||
|
>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text bg-white border-end-0">
|
<span class="input-group-text bg-white border-end-0">
|
||||||
<i class="fas fa-user text-muted"></i>
|
<i class="fas fa-user text-muted"></i>
|
||||||
@ -137,15 +192,27 @@
|
|||||||
@input="handleClientSearch"
|
@input="handleClientSearch"
|
||||||
@focus="showClientResults = true"
|
@focus="showClientResults = true"
|
||||||
/>
|
/>
|
||||||
<button v-if="selectedClient" class="btn btn-outline-secondary" type="button" @click="clearClientSelection">
|
<button
|
||||||
|
v-if="selectedClient"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearClientSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getFieldError('client')" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('client')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("client") }}
|
{{ getFieldError("client") }}
|
||||||
</div>
|
</div>
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div v-if="showClientResults && clientSearchResults.length" class="list-group position-absolute w-100 shadow mt-1" style="z-index: 1050; max-height: 200px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showClientResults && clientSearchResults.length"
|
||||||
|
class="list-group position-absolute w-100 shadow mt-1"
|
||||||
|
style="z-index: 1050; max-height: 200px; overflow-y: auto"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="c in clientSearchResults"
|
v-for="c in clientSearchResults"
|
||||||
:key="c.id"
|
:key="c.id"
|
||||||
@ -165,20 +232,27 @@
|
|||||||
<!-- ROW 3: Date et Type -->
|
<!-- ROW 3: Date et Type -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-12 mb-3">
|
<div class="col-md-12 mb-3">
|
||||||
<label class="form-label fw-bold small text-dark">Date et heure *</label>
|
<label class="form-label fw-bold small text-dark"
|
||||||
|
>Date et heure *</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="interventionForm.scheduled_at"
|
v-model="interventionForm.scheduled_at"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:class="{ 'is-invalid': hasError('scheduled_at') }"
|
:class="{ 'is-invalid': hasError('scheduled_at') }"
|
||||||
required
|
required
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="getFieldError('scheduled_at')"
|
||||||
|
class="invalid-feedback"
|
||||||
>
|
>
|
||||||
<div v-if="getFieldError('scheduled_at')" class="invalid-feedback">
|
|
||||||
{{ getFieldError("scheduled_at") }}
|
{{ getFieldError("scheduled_at") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-12 position-relative">
|
<div class="col-md-12 position-relative">
|
||||||
<label class="form-label fw-bold small text-dark">Type de soin *</label>
|
<label class="form-label fw-bold small text-dark"
|
||||||
|
>Type de soin *</label
|
||||||
|
>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text bg-white border-end-0">
|
<span class="input-group-text bg-white border-end-0">
|
||||||
<i class="fas fa-medkit text-muted"></i>
|
<i class="fas fa-medkit text-muted"></i>
|
||||||
@ -190,17 +264,29 @@
|
|||||||
:class="{ 'is-invalid': hasError('product_id') }"
|
:class="{ 'is-invalid': hasError('product_id') }"
|
||||||
placeholder="Rechercher un type de soin..."
|
placeholder="Rechercher un type de soin..."
|
||||||
@input="handleProductSearch"
|
@input="handleProductSearch"
|
||||||
@focus="showProductResults = true"
|
@focus="handleProductFocus"
|
||||||
/>
|
/>
|
||||||
<button v-if="productForm.product_id" class="btn btn-outline-secondary" type="button" @click="clearProductSelection">
|
<button
|
||||||
|
v-if="productForm.product_id"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearProductSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getFieldError('product_id')" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('product_id')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("product_id") }}
|
{{ getFieldError("product_id") }}
|
||||||
</div>
|
</div>
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div v-if="showProductResults && productSearchResults.length" class="list-group position-absolute w-100 shadow mt-1" style="z-index: 1050; max-height: 200px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showProductResults && productSearchResults.length"
|
||||||
|
class="list-group position-absolute w-100 shadow mt-1"
|
||||||
|
style="z-index: 1050; max-height: 200px; overflow-y: auto"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="p in productSearchResults"
|
v-for="p in productSearchResults"
|
||||||
:key="p.id"
|
:key="p.id"
|
||||||
@ -210,7 +296,9 @@
|
|||||||
>
|
>
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h6 class="mb-1 text-sm">{{ p.nom }}</h6>
|
<h6 class="mb-1 text-sm">{{ p.nom }}</h6>
|
||||||
<small class="text-xs text-muted">{{ p.price ? `${p.price}€` : '' }}</small>
|
<small class="text-xs text-muted">{{
|
||||||
|
p.price ? `${p.price}€` : ""
|
||||||
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -220,27 +308,47 @@
|
|||||||
<!-- ROW 4: Intervenant -->
|
<!-- ROW 4: Intervenant -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label fw-bold small text-dark">Intervenant *</label>
|
<label class="form-label fw-bold small text-dark"
|
||||||
|
>Intervenant *</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-2">
|
<div class="col-md-6 mb-2">
|
||||||
<label class="form-label text-xs text-muted mb-1">Salarié Thanatopracteur</label>
|
<label class="form-label text-xs text-muted mb-1"
|
||||||
<select v-model="interventionForm.assigned_practitioner_id" class="form-select" :class="{ 'is-invalid': hasError('assigned_practitioner_id') }">
|
>Salarié Thanatopracteur</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="interventionForm.assigned_practitioner_id"
|
||||||
|
class="form-select"
|
||||||
|
:class="{
|
||||||
|
'is-invalid': hasError('assigned_practitioner_id'),
|
||||||
|
}"
|
||||||
|
>
|
||||||
<option value="">Sélectionner un salarié</option>
|
<option value="">Sélectionner un salarié</option>
|
||||||
<option
|
<option
|
||||||
v-for="practitioner in practitioners"
|
v-for="practitioner in practitioners"
|
||||||
:key="practitioner.id"
|
:key="practitioner.id"
|
||||||
:value="practitioner.id"
|
:value="practitioner.id"
|
||||||
>
|
>
|
||||||
{{ practitioner.employee?.first_name }} {{ practitioner.employee?.last_name }}
|
{{ practitioner.employee?.first_name }}
|
||||||
|
{{ practitioner.employee?.last_name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div v-if="getFieldError('assigned_practitioner_id')" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('assigned_practitioner_id')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("assigned_practitioner_id") }}
|
{{ getFieldError("assigned_practitioner_id") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-2">
|
<div class="col-md-6 mb-2">
|
||||||
<label class="form-label text-xs text-muted mb-1">OU Sous-traitant</label>
|
<label class="form-label text-xs text-muted mb-1"
|
||||||
<select class="form-select bg-light border-0" disabled title="Bientôt disponible">
|
>OU Sous-traitant</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="form-select bg-light border-0"
|
||||||
|
disabled
|
||||||
|
title="Bientôt disponible"
|
||||||
|
>
|
||||||
<option value="">Sélectionner un sous-traitant</option>
|
<option value="">Sélectionner un sous-traitant</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -249,7 +357,9 @@
|
|||||||
<!-- ROW 5: Numéro Prescription -->
|
<!-- ROW 5: Numéro Prescription -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label fw-bold small text-dark">Numéro de prescription médicale</label>
|
<label class="form-label fw-bold small text-dark"
|
||||||
|
>Numéro de prescription médicale</label
|
||||||
|
>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text bg-white border-end-0">
|
<span class="input-group-text bg-white border-end-0">
|
||||||
<i class="fas fa-prescription text-muted"></i>
|
<i class="fas fa-prescription text-muted"></i>
|
||||||
@ -259,26 +369,42 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="form-control border-start-0"
|
class="form-control border-start-0"
|
||||||
placeholder="Ex: RX-2024-001 (si fourni par la famille)"
|
placeholder="Ex: RX-2024-001 (si fourni par la famille)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
title="Activer la saisie vocale"
|
||||||
|
@click="toggleVoiceInput('medical_prescription_number')"
|
||||||
>
|
>
|
||||||
<button class="btn btn-outline-secondary" type="button" title="Activer la saisie vocale" @click="toggleVoiceInput('medical_prescription_number')">
|
|
||||||
<i class="fas fa-microphone"></i>
|
<i class="fas fa-microphone"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-muted mt-1">📋 Optionnel - À ajouter si fourni par la famille ou dans les documents reçus</p>
|
<p class="text-xs text-muted mt-1">
|
||||||
|
📋 Optionnel - À ajouter si fourni par la famille ou dans les
|
||||||
|
documents reçus
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ROW 6: Lieu -->
|
<!-- ROW 6: Lieu -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div
|
||||||
<label class="form-label fw-bold small text-dark mb-0">Lieu d'intervention</label>
|
class="d-flex justify-content-between align-items-center mb-1"
|
||||||
|
>
|
||||||
|
<label class="form-label fw-bold small text-dark mb-0"
|
||||||
|
>Lieu d'intervention</label
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-primary py-0 px-2"
|
class="btn btn-sm btn-outline-primary py-0 px-2"
|
||||||
@click="toggleLocationMode"
|
@click="toggleLocationMode"
|
||||||
>
|
>
|
||||||
{{ locationForm.is_existing ? "+ Nouveau lieu" : "Rechercher lieu" }}
|
{{
|
||||||
|
locationForm.is_existing
|
||||||
|
? "+ Nouveau lieu"
|
||||||
|
: "Rechercher lieu"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -292,17 +418,29 @@
|
|||||||
v-model="locationSearchQuery"
|
v-model="locationSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control border-start-0"
|
class="form-control border-start-0"
|
||||||
:class="{ 'is-invalid': hasError('location.name') && !locationForm.id }"
|
:class="{
|
||||||
|
'is-invalid':
|
||||||
|
hasError('location.name') && !locationForm.id,
|
||||||
|
}"
|
||||||
placeholder="Rechercher un lieu..."
|
placeholder="Rechercher un lieu..."
|
||||||
@input="handleLocationSearch"
|
@input="handleLocationSearch"
|
||||||
@focus="showLocationResults = true"
|
@focus="showLocationResults = true"
|
||||||
/>
|
/>
|
||||||
<button v-if="locationForm.id" class="btn btn-outline-secondary" type="button" @click="clearLocationSelection">
|
<button
|
||||||
|
v-if="locationForm.id"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearLocationSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div v-if="showLocationResults && locationSearchResults.length" class="list-group position-absolute w-100 shadow mt-1" style="z-index: 1050; max-height: 200px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showLocationResults && locationSearchResults.length"
|
||||||
|
class="list-group position-absolute w-100 shadow mt-1"
|
||||||
|
style="z-index: 1050; max-height: 200px; overflow-y: auto"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="loc in locationSearchResults"
|
v-for="loc in locationSearchResults"
|
||||||
:key="loc.id"
|
:key="loc.id"
|
||||||
@ -316,7 +454,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getFieldError('location.name') && !locationForm.id" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('location.name') && !locationForm.id"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("location.name") }}
|
{{ getFieldError("location.name") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -325,24 +466,31 @@
|
|||||||
<div v-else class="card card-body bg-light border-0 p-3">
|
<div v-else class="card card-body bg-light border-0 p-3">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small text-muted mb-1">Nom du lieu *</label>
|
<label class="form-label small text-muted mb-1"
|
||||||
|
>Nom du lieu *</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="locationForm.name"
|
v-model="locationForm.name"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
placeholder="Ex: Domicile, Eglise St-Pierre..."
|
placeholder="Ex: Domicile, Eglise St-Pierre..."
|
||||||
:class="{ 'is-invalid': hasError('location.name') }"
|
:class="{ 'is-invalid': hasError('location.name') }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="getFieldError('location.name')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
>
|
>
|
||||||
<div v-if="getFieldError('location.name')" class="invalid-feedback d-block">
|
|
||||||
{{ getFieldError("location.name") }}
|
{{ getFieldError("location.name") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small text-muted mb-1">Ville</label>
|
<label class="form-label small text-muted mb-1"
|
||||||
|
>Ville</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="locationForm.city"
|
v-model="locationForm.city"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
placeholder="Ex: Paris"
|
placeholder="Ex: Paris"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -367,9 +515,13 @@
|
|||||||
<!-- ROW 8: Observations -->
|
<!-- ROW 8: Observations -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label fw-bold small text-dark">Observations techniques</label>
|
<label class="form-label fw-bold small text-dark"
|
||||||
|
>Observations techniques</label
|
||||||
|
>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text bg-white align-items-start border-end-0 pt-2">
|
<span
|
||||||
|
class="input-group-text bg-white align-items-start border-end-0 pt-2"
|
||||||
|
>
|
||||||
<i class="fas fa-sticky-note text-muted"></i>
|
<i class="fas fa-sticky-note text-muted"></i>
|
||||||
</span>
|
</span>
|
||||||
<textarea
|
<textarea
|
||||||
@ -378,7 +530,12 @@
|
|||||||
rows="4"
|
rows="4"
|
||||||
placeholder="Notes et observations..."
|
placeholder="Notes et observations..."
|
||||||
></textarea>
|
></textarea>
|
||||||
<button class="btn btn-outline-secondary align-items-start pt-2" type="button" title="Activer la saisie vocale" @click="toggleVoiceInput('notes')">
|
<button
|
||||||
|
class="btn btn-outline-secondary align-items-start pt-2"
|
||||||
|
type="button"
|
||||||
|
title="Activer la saisie vocale"
|
||||||
|
@click="toggleVoiceInput('notes')"
|
||||||
|
>
|
||||||
<i class="fas fa-microphone"></i>
|
<i class="fas fa-microphone"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -386,19 +543,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer Actions -->
|
<!-- Footer Actions -->
|
||||||
<div class="border-top pt-3 d-flex flex-column flex-md-row justify-content-between gap-2">
|
<div
|
||||||
<button type="button" class="btn btn-outline-success d-flex align-items-center justify-content-center gap-2" disabled>
|
class="border-top pt-3 d-flex flex-column flex-md-row justify-content-between gap-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-success d-flex align-items-center justify-content-center gap-2"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
<i class="fas fa-envelope"></i> Récupérer docs emails
|
<i class="fas fa-envelope"></i> Récupérer docs emails
|
||||||
</button>
|
</button>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-secondary w-100 w-md-auto" data-bs-dismiss="modal" :disabled="submitting">Annuler</button>
|
<button
|
||||||
<button type="submit" class="btn btn-primary w-100 w-md-auto text-white" :disabled="submitting">
|
type="button"
|
||||||
<span v-if="submitting" class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
class="btn btn-outline-secondary w-100 w-md-auto"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
:disabled="submitting"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary w-100 w-md-auto text-white"
|
||||||
|
:disabled="submitting"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="submitting"
|
||||||
|
class="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
{{ isEditing ? "Mettre à jour" : "Créer l'intervention" }}
|
{{ isEditing ? "Mettre à jour" : "Créer l'intervention" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -414,7 +592,7 @@ import {
|
|||||||
defineEmits,
|
defineEmits,
|
||||||
defineExpose,
|
defineExpose,
|
||||||
onMounted,
|
onMounted,
|
||||||
nextTick
|
nextTick,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import DeceasedService from "@/services/deceased";
|
import DeceasedService from "@/services/deceased";
|
||||||
@ -470,12 +648,13 @@ let clientSearchTimeout;
|
|||||||
|
|
||||||
// Product
|
// Product
|
||||||
const productForm = ref({
|
const productForm = ref({
|
||||||
product_id: null
|
product_id: null,
|
||||||
});
|
});
|
||||||
const productSearchQuery = ref("");
|
const productSearchQuery = ref("");
|
||||||
const productSearchResults = ref([]);
|
const productSearchResults = ref([]);
|
||||||
const allProducts = ref([]);
|
const allProducts = ref([]);
|
||||||
const showProductResults = ref(false);
|
const showProductResults = ref(false);
|
||||||
|
let productSearchTimeout;
|
||||||
|
|
||||||
// Location - FIXED: Start with search mode (true = search existing, false = create new)
|
// Location - FIXED: Start with search mode (true = search existing, false = create new)
|
||||||
const locationForm = ref({
|
const locationForm = ref({
|
||||||
@ -521,10 +700,17 @@ onMounted(async () => {
|
|||||||
const pService = new ProductService();
|
const pService = new ProductService();
|
||||||
const response = await pService.getAllProducts({
|
const response = await pService.getAllProducts({
|
||||||
per_page: 100,
|
per_page: 100,
|
||||||
is_intervention: true
|
is_intervention: true,
|
||||||
});
|
});
|
||||||
allProducts.value = response.data;
|
|
||||||
productSearchResults.value = allProducts.value;
|
const products = Array.isArray(response?.data)
|
||||||
|
? response.data
|
||||||
|
: Array.isArray(response?.data?.data)
|
||||||
|
? response.data.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
allProducts.value = products;
|
||||||
|
productSearchResults.value = products;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load products", e);
|
console.error("Failed to load products", e);
|
||||||
}
|
}
|
||||||
@ -561,20 +747,24 @@ const handleDeceasedSearch = () => {
|
|||||||
}
|
}
|
||||||
deceasedSearchTimeout = setTimeout(async () => {
|
deceasedSearchTimeout = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const results = await DeceasedService.searchDeceased(deceasedSearchQuery.value);
|
const results = await DeceasedService.searchDeceased(
|
||||||
|
deceasedSearchQuery.value
|
||||||
|
);
|
||||||
deceasedSearchResults.value = results;
|
deceasedSearchResults.value = results;
|
||||||
} catch(e) { console.error(e); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectDeceased = (d) => {
|
const selectDeceased = (d) => {
|
||||||
deceasedForm.value.id = d.id;
|
deceasedForm.value.id = d.id;
|
||||||
deceasedSearchQuery.value = `${d.first_name || ''} ${d.last_name}`;
|
deceasedSearchQuery.value = `${d.first_name || ""} ${d.last_name}`;
|
||||||
deceasedSearchResults.value = [];
|
deceasedSearchResults.value = [];
|
||||||
showDeceasedResults.value = false;
|
showDeceasedResults.value = false;
|
||||||
|
|
||||||
// Clear any error for deceased
|
// Clear any error for deceased
|
||||||
errors.value = errors.value.filter(e => e.field !== 'deceased_id');
|
errors.value = errors.value.filter((e) => e.field !== "deceased_id");
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearDeceasedSelection = () => {
|
const clearDeceasedSelection = () => {
|
||||||
@ -587,9 +777,8 @@ const clearDeceasedSelection = () => {
|
|||||||
deceasedSearchResults.value = [];
|
deceasedSearchResults.value = [];
|
||||||
|
|
||||||
// Clear errors
|
// Clear errors
|
||||||
errors.value = errors.value.filter(e =>
|
errors.value = errors.value.filter(
|
||||||
e.field !== 'deceased_id' &&
|
(e) => e.field !== "deceased_id" && e.field !== "deceased.last_name"
|
||||||
e.field !== 'deceased.last_name'
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -602,20 +791,24 @@ const handleClientSearch = () => {
|
|||||||
}
|
}
|
||||||
clientSearchTimeout = setTimeout(async () => {
|
clientSearchTimeout = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const results = await ClientService.searchClients(clientSearchQuery.value);
|
const results = await ClientService.searchClients(
|
||||||
|
clientSearchQuery.value
|
||||||
|
);
|
||||||
clientSearchResults.value = results;
|
clientSearchResults.value = results;
|
||||||
} catch(e) { console.error(e); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectClient = (c) => {
|
const selectClient = (c) => {
|
||||||
selectedClient.value = c;
|
selectedClient.value = c;
|
||||||
clientSearchQuery.value = c.name + (c.email ? ` (${c.email})` : '');
|
clientSearchQuery.value = c.name + (c.email ? ` (${c.email})` : "");
|
||||||
clientSearchResults.value = [];
|
clientSearchResults.value = [];
|
||||||
showClientResults.value = false;
|
showClientResults.value = false;
|
||||||
|
|
||||||
// Clear any error for client
|
// Clear any error for client
|
||||||
errors.value = errors.value.filter(e => e.field !== 'client');
|
errors.value = errors.value.filter((e) => e.field !== "client");
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearClientSelection = () => {
|
const clearClientSelection = () => {
|
||||||
@ -624,19 +817,45 @@ const clearClientSelection = () => {
|
|||||||
clientSearchResults.value = [];
|
clientSearchResults.value = [];
|
||||||
|
|
||||||
// Clear error
|
// Clear error
|
||||||
errors.value = errors.value.filter(e => e.field !== 'client');
|
errors.value = errors.value.filter((e) => e.field !== "client");
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- PRODUCT LOGIC ---
|
// --- PRODUCT LOGIC ---
|
||||||
const handleProductSearch = () => {
|
const handleProductSearch = () => {
|
||||||
const query = productSearchQuery.value.toLowerCase();
|
showProductResults.value = true;
|
||||||
if (!query) {
|
|
||||||
productSearchResults.value = allProducts.value;
|
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
||||||
return;
|
|
||||||
|
productSearchTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const pService = new ProductService();
|
||||||
|
const response = await pService.getAllProducts({
|
||||||
|
per_page: 100,
|
||||||
|
is_intervention: true,
|
||||||
|
search: productSearchQuery.value || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = Array.isArray(response?.data)
|
||||||
|
? response.data
|
||||||
|
: Array.isArray(response?.data?.data)
|
||||||
|
? response.data.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
productSearchResults.value = products;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to search intervention products", e);
|
||||||
|
productSearchResults.value = [];
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProductFocus = () => {
|
||||||
|
showProductResults.value = true;
|
||||||
|
if (!productSearchQuery.value) {
|
||||||
|
productSearchResults.value = allProducts.value;
|
||||||
|
} else {
|
||||||
|
handleProductSearch();
|
||||||
}
|
}
|
||||||
productSearchResults.value = allProducts.value.filter(p =>
|
|
||||||
p.nom.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectProduct = (p) => {
|
const selectProduct = (p) => {
|
||||||
@ -645,10 +864,10 @@ const selectProduct = (p) => {
|
|||||||
showProductResults.value = false;
|
showProductResults.value = false;
|
||||||
|
|
||||||
// Clear error
|
// Clear error
|
||||||
errors.value = errors.value.filter(e => e.field !== 'product_id');
|
errors.value = errors.value.filter((e) => e.field !== "product_id");
|
||||||
|
|
||||||
if (!interventionForm.value.type) {
|
if (!interventionForm.value.type) {
|
||||||
interventionForm.value.type = 'thanatopraxie';
|
interventionForm.value.type = "thanatopraxie";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -658,7 +877,7 @@ const clearProductSelection = () => {
|
|||||||
productSearchResults.value = allProducts.value;
|
productSearchResults.value = allProducts.value;
|
||||||
|
|
||||||
// Clear error
|
// Clear error
|
||||||
errors.value = errors.value.filter(e => e.field !== 'product_id');
|
errors.value = errors.value.filter((e) => e.field !== "product_id");
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- LOCATION LOGIC --- (FIXED)
|
// --- LOCATION LOGIC --- (FIXED)
|
||||||
@ -676,7 +895,7 @@ const toggleLocationMode = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear location errors
|
// Clear location errors
|
||||||
errors.value = errors.value.filter(e => e.field === 'location.name');
|
errors.value = errors.value.filter((e) => e.field === "location.name");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLocationSearch = () => {
|
const handleLocationSearch = () => {
|
||||||
@ -689,10 +908,12 @@ const handleLocationSearch = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await ClientLocationService.getAllClientLocations({
|
const response = await ClientLocationService.getAllClientLocations({
|
||||||
search: locationSearchQuery.value,
|
search: locationSearchQuery.value,
|
||||||
per_page: 10
|
per_page: 10,
|
||||||
});
|
});
|
||||||
locationSearchResults.value = response.data;
|
locationSearchResults.value = response.data;
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -707,7 +928,7 @@ const selectLocation = (loc) => {
|
|||||||
locationSearchResults.value = [];
|
locationSearchResults.value = [];
|
||||||
|
|
||||||
// Clear error
|
// Clear error
|
||||||
errors.value = errors.value.filter(e => e.field !== 'location.name');
|
errors.value = errors.value.filter((e) => e.field !== "location.name");
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearLocationSelection = () => {
|
const clearLocationSelection = () => {
|
||||||
@ -719,7 +940,7 @@ const clearLocationSelection = () => {
|
|||||||
showLocationResults.value = false;
|
showLocationResults.value = false;
|
||||||
|
|
||||||
// Clear error
|
// Clear error
|
||||||
errors.value = errors.value.filter(e => e.field !== 'location.name');
|
errors.value = errors.value.filter((e) => e.field !== "location.name");
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- VOICE INPUT ---
|
// --- VOICE INPUT ---
|
||||||
@ -729,8 +950,9 @@ const toggleVoiceInput = (field) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- VALIDATION ---
|
// --- VALIDATION ---
|
||||||
const hasError = (field) => errors.value.some(e => e.field === field);
|
const hasError = (field) => errors.value.some((e) => e.field === field);
|
||||||
const getFieldError = (field) => errors.value.find(e => e.field === field)?.message || "";
|
const getFieldError = (field) =>
|
||||||
|
errors.value.find((e) => e.field === field)?.message || "";
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
errors.value = [];
|
errors.value = [];
|
||||||
@ -739,47 +961,65 @@ const validate = () => {
|
|||||||
// Deceased
|
// Deceased
|
||||||
if (deceasedForm.value.is_existing) {
|
if (deceasedForm.value.is_existing) {
|
||||||
if (!deceasedForm.value.id) {
|
if (!deceasedForm.value.id) {
|
||||||
errors.value.push({ field: 'deceased_id', message: 'Veuillez sélectionner un défunt.' });
|
errors.value.push({
|
||||||
|
field: "deceased_id",
|
||||||
|
message: "Veuillez sélectionner un défunt.",
|
||||||
|
});
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!deceasedForm.value.last_name) {
|
if (!deceasedForm.value.last_name) {
|
||||||
errors.value.push({ field: 'deceased.last_name', message: 'Le nom du défunt est requis.' });
|
errors.value.push({
|
||||||
|
field: "deceased.last_name",
|
||||||
|
message: "Le nom du défunt est requis.",
|
||||||
|
});
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
if (!selectedClient.value) {
|
if (!selectedClient.value) {
|
||||||
errors.value.push({ field: 'client', message: "Veuillez sélectionner un client." });
|
errors.value.push({
|
||||||
|
field: "client",
|
||||||
|
message: "Veuillez sélectionner un client.",
|
||||||
|
});
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intervention fields
|
// Intervention fields
|
||||||
if (!interventionForm.value.scheduled_at) {
|
if (!interventionForm.value.scheduled_at) {
|
||||||
errors.value.push({ field: 'scheduled_at', message: "Date obligatoire." });
|
errors.value.push({ field: "scheduled_at", message: "Date obligatoire." });
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!productForm.value.product_id) {
|
if (!productForm.value.product_id) {
|
||||||
errors.value.push({ field: 'product_id', message: "Type de soin requis." });
|
errors.value.push({ field: "product_id", message: "Type de soin requis." });
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!interventionForm.value.assigned_practitioner_id) {
|
if (!interventionForm.value.assigned_practitioner_id) {
|
||||||
errors.value.push({ field: 'assigned_practitioner_id', message: "Intervenant requis." });
|
errors.value.push({
|
||||||
|
field: "assigned_practitioner_id",
|
||||||
|
message: "Intervenant requis.",
|
||||||
|
});
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
if (locationForm.value.is_existing) {
|
if (locationForm.value.is_existing) {
|
||||||
if (!locationForm.value.id) {
|
if (!locationForm.value.id) {
|
||||||
errors.value.push({ field: 'location.name', message: "Veuillez sélectionner un lieu." });
|
errors.value.push({
|
||||||
|
field: "location.name",
|
||||||
|
message: "Veuillez sélectionner un lieu.",
|
||||||
|
});
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!locationForm.value.name) {
|
if (!locationForm.value.name) {
|
||||||
errors.value.push({ field: 'location.name', message: "Le nom du lieu est requis." });
|
errors.value.push({
|
||||||
|
field: "location.name",
|
||||||
|
message: "Le nom du lieu est requis.",
|
||||||
|
});
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -805,30 +1045,63 @@ const handleSubmit = async () => {
|
|||||||
if (deceasedForm.value.is_existing && deceasedForm.value.id) {
|
if (deceasedForm.value.is_existing && deceasedForm.value.id) {
|
||||||
formData.append("deceased_id", deceasedForm.value.id);
|
formData.append("deceased_id", deceasedForm.value.id);
|
||||||
} else {
|
} else {
|
||||||
formData.append(`deceased[first_name]`, deceasedForm.value.first_name || "");
|
formData.append(
|
||||||
formData.append(`deceased[last_name]`, deceasedForm.value.last_name || "");
|
`deceased[first_name]`,
|
||||||
if (deceasedForm.value.birth_date) formData.append(`deceased[birth_date]`, deceasedForm.value.birth_date);
|
deceasedForm.value.first_name || ""
|
||||||
if (deceasedForm.value.death_date) formData.append(`deceased[death_date]`, deceasedForm.value.death_date);
|
);
|
||||||
|
formData.append(
|
||||||
|
`deceased[last_name]`,
|
||||||
|
deceasedForm.value.last_name || ""
|
||||||
|
);
|
||||||
|
if (deceasedForm.value.birth_date)
|
||||||
|
formData.append(`deceased[birth_date]`, deceasedForm.value.birth_date);
|
||||||
|
if (deceasedForm.value.death_date)
|
||||||
|
formData.append(`deceased[death_date]`, deceasedForm.value.death_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
if (selectedClient.value) {
|
if (selectedClient.value) {
|
||||||
formData.append("client_id", selectedClient.value.id);
|
formData.append("client_id", selectedClient.value.id);
|
||||||
|
|
||||||
if (selectedClient.value.name) formData.append("client[name]", selectedClient.value.name);
|
if (selectedClient.value.name)
|
||||||
if (selectedClient.value.email) formData.append("client[email]", selectedClient.value.email);
|
formData.append("client[name]", selectedClient.value.name);
|
||||||
if (selectedClient.value.phone) formData.append("client[phone]", selectedClient.value.phone);
|
if (selectedClient.value.email)
|
||||||
|
formData.append("client[email]", selectedClient.value.email);
|
||||||
|
if (selectedClient.value.phone)
|
||||||
|
formData.append("client[phone]", selectedClient.value.phone);
|
||||||
|
|
||||||
if (selectedClient.value.billing_address) {
|
if (selectedClient.value.billing_address) {
|
||||||
if (selectedClient.value.billing_address.line1) formData.append("client[billing_address_line1]", selectedClient.value.billing_address.line1);
|
if (selectedClient.value.billing_address.line1)
|
||||||
if (selectedClient.value.billing_address.line2) formData.append("client[billing_address_line2]", selectedClient.value.billing_address.line2);
|
formData.append(
|
||||||
if (selectedClient.value.billing_address.postal_code) formData.append("client[billing_postal_code]", selectedClient.value.billing_address.postal_code);
|
"client[billing_address_line1]",
|
||||||
if (selectedClient.value.billing_address.city) formData.append("client[billing_city]", selectedClient.value.billing_address.city);
|
selectedClient.value.billing_address.line1
|
||||||
if (selectedClient.value.billing_address.country_code) formData.append("client[billing_country_code]", selectedClient.value.billing_address.country_code);
|
);
|
||||||
|
if (selectedClient.value.billing_address.line2)
|
||||||
|
formData.append(
|
||||||
|
"client[billing_address_line2]",
|
||||||
|
selectedClient.value.billing_address.line2
|
||||||
|
);
|
||||||
|
if (selectedClient.value.billing_address.postal_code)
|
||||||
|
formData.append(
|
||||||
|
"client[billing_postal_code]",
|
||||||
|
selectedClient.value.billing_address.postal_code
|
||||||
|
);
|
||||||
|
if (selectedClient.value.billing_address.city)
|
||||||
|
formData.append(
|
||||||
|
"client[billing_city]",
|
||||||
|
selectedClient.value.billing_address.city
|
||||||
|
);
|
||||||
|
if (selectedClient.value.billing_address.country_code)
|
||||||
|
formData.append(
|
||||||
|
"client[billing_country_code]",
|
||||||
|
selectedClient.value.billing_address.country_code
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedClient.value.vat_number) formData.append("client[vat_number]", selectedClient.value.vat_number);
|
if (selectedClient.value.vat_number)
|
||||||
if (selectedClient.value.siret) formData.append("client[siret]", selectedClient.value.siret);
|
formData.append("client[vat_number]", selectedClient.value.vat_number);
|
||||||
|
if (selectedClient.value.siret)
|
||||||
|
formData.append("client[siret]", selectedClient.value.siret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
@ -836,7 +1109,8 @@ const handleSubmit = async () => {
|
|||||||
formData.append("location_id", locationForm.value.id);
|
formData.append("location_id", locationForm.value.id);
|
||||||
} else {
|
} else {
|
||||||
formData.append("location[name]", locationForm.value.name || "");
|
formData.append("location[name]", locationForm.value.name || "");
|
||||||
if (locationForm.value.city) formData.append("location[city]", locationForm.value.city);
|
if (locationForm.value.city)
|
||||||
|
formData.append("location[city]", locationForm.value.city);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Product
|
// Product
|
||||||
@ -868,17 +1142,25 @@ const handleSubmit = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes to clear errors
|
// Watch for changes to clear errors
|
||||||
watch(() => locationForm.value.name, () => {
|
watch(
|
||||||
if (getFieldError('location.name')) {
|
() => locationForm.value.name,
|
||||||
errors.value = errors.value.filter(e => e.field !== 'location.name');
|
() => {
|
||||||
|
if (getFieldError("location.name")) {
|
||||||
|
errors.value = errors.value.filter((e) => e.field !== "location.name");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(() => interventionForm.value.assigned_practitioner_id, () => {
|
watch(
|
||||||
if (getFieldError('assigned_practitioner_id')) {
|
() => interventionForm.value.assigned_practitioner_id,
|
||||||
errors.value = errors.value.filter(e => e.field !== 'assigned_practitioner_id');
|
() => {
|
||||||
|
if (getFieldError("assigned_practitioner_id")) {
|
||||||
|
errors.value = errors.value.filter(
|
||||||
|
(e) => e.field !== "assigned_practitioner_id"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
|
|||||||
@ -6,26 +6,33 @@
|
|||||||
<div class="col-12 d-flex justify-content-center">
|
<div class="col-12 d-flex justify-content-center">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<input
|
<input
|
||||||
|
id="modeNew"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="btn-check"
|
class="btn-check"
|
||||||
name="deceasedMode"
|
name="deceasedMode"
|
||||||
id="modeNew"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:checked="!formData.is_existing"
|
:checked="!formData.is_existing"
|
||||||
@change="formData.is_existing = false; formData.id = null"
|
@change="
|
||||||
|
formData.is_existing = false;
|
||||||
|
formData.id = null;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="modeNew"
|
||||||
|
>Nouveau Défunt</label
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline-primary" for="modeNew">Nouveau Défunt</label>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
id="modeSearch"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="btn-check"
|
class="btn-check"
|
||||||
name="deceasedMode"
|
name="deceasedMode"
|
||||||
id="modeSearch"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:checked="formData.is_existing"
|
:checked="formData.is_existing"
|
||||||
@change="formData.is_existing = true"
|
@change="formData.is_existing = true"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="modeSearch"
|
||||||
|
>Rechercher</label
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline-primary" for="modeSearch">Rechercher</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,16 +51,28 @@
|
|||||||
placeholder="Tapez pour rechercher..."
|
placeholder="Tapez pour rechercher..."
|
||||||
@input="handleSearchInput"
|
@input="handleSearchInput"
|
||||||
/>
|
/>
|
||||||
<button v-if="formData.id" class="btn btn-outline-secondary" type="button" @click="clearSelection">
|
<button
|
||||||
|
v-if="formData.id"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getFieldError('deceased_id')" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('deceased_id')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("deceased_id") }}
|
{{ getFieldError("deceased_id") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dropdown Results -->
|
<!-- Dropdown Results -->
|
||||||
<div v-if="showResults && searchResults.length" class="list-group position-absolute w-100 shadow" style="z-index: 1000; max-height: 200px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showResults && searchResults.length"
|
||||||
|
class="list-group position-absolute w-100 shadow"
|
||||||
|
style="z-index: 1000; max-height: 200px; overflow-y: auto"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="deceased in searchResults"
|
v-for="deceased in searchResults"
|
||||||
:key="deceased.id"
|
:key="deceased.id"
|
||||||
@ -62,19 +81,33 @@
|
|||||||
@click="selectDeceased(deceased)"
|
@click="selectDeceased(deceased)"
|
||||||
>
|
>
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h6 class="mb-1">{{ deceased.first_name }} {{ deceased.last_name }}</h6>
|
<h6 class="mb-1">
|
||||||
<small>{{ deceased.birth_date }} - {{ deceased.death_date }}</small>
|
{{ deceased.first_name }} {{ deceased.last_name }}
|
||||||
|
</h6>
|
||||||
|
<small
|
||||||
|
>{{ deceased.birth_date }} - {{ deceased.death_date }}</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showResults && searchResults.length === 0 && searchQuery.length >= 2 && !isSearching" class="list-group position-absolute w-100 shadow" style="z-index: 1000;">
|
<div
|
||||||
|
v-if="
|
||||||
|
showResults &&
|
||||||
|
searchResults.length === 0 &&
|
||||||
|
searchQuery.length >= 2 &&
|
||||||
|
!isSearching
|
||||||
|
"
|
||||||
|
class="list-group position-absolute w-100 shadow"
|
||||||
|
style="z-index: 1000"
|
||||||
|
>
|
||||||
<div class="list-group-item text-muted">Aucun résultat trouvé.</div>
|
<div class="list-group-item text-muted">Aucun résultat trouvé.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="formData.id" class="col-12">
|
<div v-if="formData.id" class="col-12">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<strong>Défunt sélectionné:</strong> {{ formData.first_name }} {{ formData.last_name }}
|
<strong>Défunt sélectionné:</strong> {{ formData.first_name }}
|
||||||
|
{{ formData.last_name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,26 +6,33 @@
|
|||||||
<div class="col-12 d-flex justify-content-center">
|
<div class="col-12 d-flex justify-content-center">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<input
|
<input
|
||||||
|
id="locModeNew"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="btn-check"
|
class="btn-check"
|
||||||
name="locationMode"
|
name="locationMode"
|
||||||
id="locModeNew"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:checked="!formData.is_existing"
|
:checked="!formData.is_existing"
|
||||||
@change="formData.is_existing = false; formData.id = null"
|
@change="
|
||||||
|
formData.is_existing = false;
|
||||||
|
formData.id = null;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="locModeNew"
|
||||||
|
>Nouveau Lieu</label
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline-primary" for="locModeNew">Nouveau Lieu</label>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
id="locModeSearch"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="btn-check"
|
class="btn-check"
|
||||||
name="locationMode"
|
name="locationMode"
|
||||||
id="locModeSearch"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:checked="formData.is_existing"
|
:checked="formData.is_existing"
|
||||||
@change="formData.is_existing = true"
|
@change="formData.is_existing = true"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary" for="locModeSearch"
|
||||||
|
>Rechercher</label
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline-primary" for="locModeSearch">Rechercher</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,16 +51,28 @@
|
|||||||
placeholder="Hôpital, Funérarium, Ville..."
|
placeholder="Hôpital, Funérarium, Ville..."
|
||||||
@input="handleSearchInput"
|
@input="handleSearchInput"
|
||||||
/>
|
/>
|
||||||
<button v-if="formData.id" class="btn btn-outline-secondary" type="button" @click="clearSelection">
|
<button
|
||||||
|
v-if="formData.id"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getFieldError('location_id')" class="invalid-feedback d-block">
|
<div
|
||||||
|
v-if="getFieldError('location_id')"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
{{ getFieldError("location_id") }}
|
{{ getFieldError("location_id") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dropdown Results -->
|
<!-- Dropdown Results -->
|
||||||
<div v-if="showResults && searchResults.length" class="list-group position-absolute w-100 shadow" style="z-index: 1000; max-height: 200px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showResults && searchResults.length"
|
||||||
|
class="list-group position-absolute w-100 shadow"
|
||||||
|
style="z-index: 1000; max-height: 200px; overflow-y: auto"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="loc in searchResults"
|
v-for="loc in searchResults"
|
||||||
:key="loc.id"
|
:key="loc.id"
|
||||||
@ -62,13 +81,22 @@
|
|||||||
@click="selectLocation(loc)"
|
@click="selectLocation(loc)"
|
||||||
>
|
>
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h6 class="mb-1">{{ loc.name || 'Lieu sans nom' }}</h6>
|
<h6 class="mb-1">{{ loc.name || "Lieu sans nom" }}</h6>
|
||||||
<small>{{ loc.city }}</small>
|
<small>{{ loc.city }}</small>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">{{ loc.address_line1 }}</small>
|
<small class="text-muted">{{ loc.address_line1 }}</small>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showResults && searchResults.length === 0 && searchQuery.length >= 2 && !isSearching" class="list-group position-absolute w-100 shadow" style="z-index: 1000;">
|
<div
|
||||||
|
v-if="
|
||||||
|
showResults &&
|
||||||
|
searchResults.length === 0 &&
|
||||||
|
searchQuery.length >= 2 &&
|
||||||
|
!isSearching
|
||||||
|
"
|
||||||
|
class="list-group position-absolute w-100 shadow"
|
||||||
|
style="z-index: 1000"
|
||||||
|
>
|
||||||
<div class="list-group-item text-muted">Aucun résultat trouvé.</div>
|
<div class="list-group-item text-muted">Aucun résultat trouvé.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +104,10 @@
|
|||||||
<div v-if="formData.id" class="col-12">
|
<div v-if="formData.id" class="col-12">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<strong>Lieu sélectionné:</strong> {{ formData.name }} <br />
|
<strong>Lieu sélectionné:</strong> {{ formData.name }} <br />
|
||||||
<small>{{ formData.address }}, {{ formData.postal_code }} {{ formData.city }}</small>
|
<small
|
||||||
|
>{{ formData.address }}, {{ formData.postal_code }}
|
||||||
|
{{ formData.city }}</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -247,7 +278,7 @@ const handleSearchInput = () => {
|
|||||||
// Use getAllClientLocations with search param
|
// Use getAllClientLocations with search param
|
||||||
const response = await ClientLocationService.getAllClientLocations({
|
const response = await ClientLocationService.getAllClientLocations({
|
||||||
search: searchQuery.value,
|
search: searchQuery.value,
|
||||||
per_page: 10
|
per_page: 10,
|
||||||
});
|
});
|
||||||
searchResults.value = response.data;
|
searchResults.value = response.data;
|
||||||
showResults.value = true;
|
showResults.value = true;
|
||||||
@ -280,7 +311,6 @@ const clearSelection = () => {
|
|||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getFieldError = (field) => {
|
const getFieldError = (field) => {
|
||||||
const error = props.errors.find((err) => err.field === field);
|
const error = props.errors.find((err) => err.field === field);
|
||||||
return error ? error.message : "";
|
return error ? error.message : "";
|
||||||
|
|||||||
@ -16,7 +16,12 @@
|
|||||||
@input="handleSearchInput"
|
@input="handleSearchInput"
|
||||||
@focus="showResults = true"
|
@focus="showResults = true"
|
||||||
/>
|
/>
|
||||||
<button v-if="formData.product_id" class="btn btn-outline-secondary" type="button" @click="clearSelection">
|
<button
|
||||||
|
v-if="formData.product_id"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -29,13 +34,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dropdown Results -->
|
<!-- Dropdown Results -->
|
||||||
<div v-if="showResults" class="list-group position-absolute w-100 shadow" style="z-index: 1000; max-height: 250px; overflow-y: auto;">
|
<div
|
||||||
|
v-if="showResults"
|
||||||
|
class="list-group position-absolute w-100 shadow"
|
||||||
|
style="z-index: 1000; max-height: 250px; overflow-y: auto"
|
||||||
|
>
|
||||||
<div v-if="loading" class="list-group-item text-center">
|
<div v-if="loading" class="list-group-item text-center">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
|
||||||
v-for="product in searchResults"
|
v-for="product in searchResults"
|
||||||
|
v-else
|
||||||
:key="product.id"
|
:key="product.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="list-group-item list-group-item-action"
|
class="list-group-item list-group-item-action"
|
||||||
@ -47,14 +59,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="text-muted">{{ product.description }}</small>
|
<small class="text-muted">{{ product.description }}</small>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!loading && searchResults.length === 0" class="list-group-item text-muted">Aucun résultat trouvé.</div>
|
<div
|
||||||
|
v-if="!loading && searchResults.length === 0"
|
||||||
|
class="list-group-item text-muted"
|
||||||
|
>
|
||||||
|
Aucun résultat trouvé.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selected Product Display -->
|
<!-- Selected Product Display -->
|
||||||
<div v-if="formData.product_id && selectedProductDisplay" class="alert alert-info mt-3">
|
<div
|
||||||
|
v-if="formData.product_id && selectedProductDisplay"
|
||||||
|
class="alert alert-info mt-3"
|
||||||
|
>
|
||||||
<strong>Soin sélectionné:</strong> {{ selectedProductDisplay }}
|
<strong>Soin sélectionné:</strong> {{ selectedProductDisplay }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -134,7 +153,8 @@ const filterProducts = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = searchQuery.value.toLowerCase();
|
const query = searchQuery.value.toLowerCase();
|
||||||
searchResults.value = allProducts.value.filter(p =>
|
searchResults.value = allProducts.value.filter(
|
||||||
|
(p) =>
|
||||||
p.nom.toLowerCase().includes(query) ||
|
p.nom.toLowerCase().includes(query) ||
|
||||||
(p.reference && p.reference.toLowerCase().includes(query)) ||
|
(p.reference && p.reference.toLowerCase().includes(query)) ||
|
||||||
(p.description && p.description.toLowerCase().includes(query))
|
(p.description && p.description.toLowerCase().includes(query))
|
||||||
@ -155,7 +175,7 @@ onMounted(async () => {
|
|||||||
// Based on previous code, it seems supported.
|
// Based on previous code, it seems supported.
|
||||||
const response = await service.getAllProducts({
|
const response = await service.getAllProducts({
|
||||||
per_page: 100, // Fetch enough
|
per_page: 100, // Fetch enough
|
||||||
is_intervention: true // Assuming backend handles this param as before
|
is_intervention: true, // Assuming backend handles this param as before
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check response structure. product.ts says ProductListResponse { data: Product[] ... }
|
// Check response structure. product.ts says ProductListResponse { data: Product[] ... }
|
||||||
|
|||||||
@ -38,11 +38,15 @@
|
|||||||
<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">Client</label>
|
<label class="form-label">Client</label>
|
||||||
<div class="info-value">{{ avoir.client?.name || 'Client inconnu' }}</div>
|
<div class="info-value">
|
||||||
|
{{ avoir.client?.name || "Client inconnu" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Facture d'origine</label>
|
<label class="form-label">Facture d'origine</label>
|
||||||
<div class="info-value">{{ avoir.invoice?.invoice_number || 'Non spécifiée' }}</div>
|
<div class="info-value">
|
||||||
|
{{ avoir.invoice?.invoice_number || "Non spécifiée" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -51,11 +55,15 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Motif</label>
|
<label class="form-label">Motif</label>
|
||||||
<div class="info-value">{{ getReasonLabel(avoir.reason_type) }}</div>
|
<div class="info-value">
|
||||||
|
{{ getReasonLabel(avoir.reason_type) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Mode de remboursement</label>
|
<label class="form-label">Mode de remboursement</label>
|
||||||
<div class="info-value">{{ getRefundMethodLabel(avoir.refund_method) }}</div>
|
<div class="info-value">
|
||||||
|
{{ getRefundMethodLabel(avoir.refund_method) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -71,11 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lines-container">
|
<div class="lines-container">
|
||||||
<div
|
<div v-for="line in avoir.lines" :key="line.id" class="line-item">
|
||||||
v-for="line in avoir.lines"
|
|
||||||
:key="line.id"
|
|
||||||
class="line-item"
|
|
||||||
>
|
|
||||||
<div class="row g-2 align-items-center">
|
<div class="row g-2 align-items-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label text-xs">Désignation</label>
|
<label class="form-label text-xs">Désignation</label>
|
||||||
@ -89,7 +93,9 @@
|
|||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Prix HT</label>
|
<label class="form-label text-xs">Prix HT</label>
|
||||||
<div class="line-price">{{ formatCurrency(line.unit_price) }}</div>
|
<div class="line-price">
|
||||||
|
{{ formatCurrency(line.unit_price) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 d-flex flex-column align-items-end">
|
<div class="col-md-2 d-flex flex-column align-items-end">
|
||||||
@ -116,7 +122,9 @@
|
|||||||
</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(avoir.total_ttc) }}</span>
|
<span class="total-amount">{{
|
||||||
|
formatCurrency(avoir.total_ttc)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -135,13 +143,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Contact client</label>
|
<label class="form-label">Contact client</label>
|
||||||
<div class="info-value">{{ avoir.client?.email || avoir.client?.phone || 'Non spécifié' }}</div>
|
<div class="info-value">
|
||||||
|
{{ avoir.client?.email || avoir.client?.phone || "Non spécifié" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="avoir.reason_description" class="mt-3">
|
<div v-if="avoir.reason_description" class="mt-3">
|
||||||
<label class="form-label">Détail du motif</label>
|
<label class="form-label">Détail du motif</label>
|
||||||
<div class="info-value notes-content">{{ avoir.reason_description }}</div>
|
<div class="info-value notes-content">
|
||||||
|
{{ avoir.reason_description }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -151,8 +163,8 @@
|
|||||||
<soft-button
|
<soft-button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
@click="dropdownOpen = !dropdownOpen"
|
|
||||||
class="btn-status"
|
class="btn-status"
|
||||||
|
@click="dropdownOpen = !dropdownOpen"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exchange-alt me-2"></i>
|
<i class="fas fa-exchange-alt me-2"></i>
|
||||||
Changer le statut
|
Changer le statut
|
||||||
@ -161,14 +173,17 @@
|
|||||||
<ul
|
<ul
|
||||||
v-if="dropdownOpen"
|
v-if="dropdownOpen"
|
||||||
class="dropdown-menu show position-absolute"
|
class="dropdown-menu show position-absolute"
|
||||||
style="top: 100%; left: 0; z-index: 1000;"
|
style="top: 100%; left: 0; z-index: 1000"
|
||||||
>
|
>
|
||||||
<li v-for="status in availableStatuses" :key="status">
|
<li v-for="status in availableStatuses" :key="status">
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
:class="{ active: status === avoir.status }"
|
:class="{ active: status === avoir.status }"
|
||||||
href="javascript:;"
|
href="javascript:;"
|
||||||
@click="changeStatus(status); dropdownOpen = false;"
|
@click="
|
||||||
|
changeStatus(status);
|
||||||
|
dropdownOpen = false;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<i :class="getStatusIcon(status) + ' me-2'"></i>
|
<i :class="getStatusIcon(status) + ' me-2'"></i>
|
||||||
{{ getStatusLabel(status) }}
|
{{ getStatusLabel(status) }}
|
||||||
@ -279,9 +294,12 @@ const changeStatus = async (newStatus) => {
|
|||||||
try {
|
try {
|
||||||
await avoirStore.updateAvoir({
|
await avoirStore.updateAvoir({
|
||||||
id: avoir.value.id,
|
id: avoir.value.id,
|
||||||
status: newStatus
|
status: newStatus,
|
||||||
});
|
});
|
||||||
notificationStore.success("Succès", `Statut mis à jour : ${getStatusLabel(newStatus)}`);
|
notificationStore.success(
|
||||||
|
"Succès",
|
||||||
|
`Statut mis à jour : ${getStatusLabel(newStatus)}`
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
|
notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,7 +76,10 @@ const handleExport = () => {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
link.setAttribute("href", url);
|
link.setAttribute("href", url);
|
||||||
link.setAttribute("download", `avoirs-export-${new Date().toISOString().split('T')[0]}.csv`);
|
link.setAttribute(
|
||||||
|
"download",
|
||||||
|
`avoirs-export-${new Date().toISOString().split("T")[0]}.csv`
|
||||||
|
);
|
||||||
link.style.visibility = "hidden";
|
link.style.visibility = "hidden";
|
||||||
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
@ -102,7 +105,10 @@ const handleDelete = async (id) => {
|
|||||||
await avoirStore.deleteAvoir(id);
|
await avoirStore.deleteAvoir(id);
|
||||||
notificationStore.success("Succès", "Avoir supprimé avec succès");
|
notificationStore.success("Succès", "Avoir supprimé avec succès");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notificationStore.error("Erreur", "Erreur lors de la suppression de l'avoir");
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Erreur lors de la suppression de l'avoir"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,12 @@
|
|||||||
<div class="card-header pb-0 p-3">
|
<div class="card-header pb-0 p-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0">Créer un nouvel avoir</h6>
|
<h6 class="mb-0">Créer un nouvel avoir</h6>
|
||||||
<soft-button color="secondary" variant="outline" size="sm" @click="goBack">
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
<i class="fas fa-arrow-left me-2"></i>Retour
|
<i class="fas fa-arrow-left me-2"></i>Retour
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
@ -45,7 +50,7 @@ const handleSubmit = async (formData) => {
|
|||||||
avoir_date: formData.date,
|
avoir_date: formData.date,
|
||||||
reason_type: formData.reason,
|
reason_type: formData.reason,
|
||||||
reason_description: formData.reasonDetail,
|
reason_description: formData.reasonDetail,
|
||||||
lines: formData.lines.map(line => ({
|
lines: formData.lines.map((line) => ({
|
||||||
description: line.designation,
|
description: line.designation,
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
unit_price: line.priceHt,
|
unit_price: line.priceHt,
|
||||||
@ -54,7 +59,10 @@ const handleSubmit = async (formData) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await avoirStore.createAvoir(payload);
|
await avoirStore.createAvoir(payload);
|
||||||
notificationStore.success("Succès", `Avoir créé avec succès: ${formData.number}`);
|
notificationStore.success(
|
||||||
|
"Succès",
|
||||||
|
`Avoir créé avec succès: ${formData.number}`
|
||||||
|
);
|
||||||
router.push("/avoirs");
|
router.push("/avoirs");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error creating avoir:", err);
|
console.error("Error creating avoir:", err);
|
||||||
|
|||||||
@ -55,8 +55,8 @@ const props = defineProps({
|
|||||||
// We need to accept pagination as a prop if it is passed from the view
|
// We need to accept pagination as a prop if it is passed from the view
|
||||||
pagination: {
|
pagination: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({}),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const goToClient = () => {
|
const goToClient = () => {
|
||||||
@ -82,6 +82,10 @@ const onPerPageChange = (perPage) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSearch = (query) => {
|
const onSearch = (query) => {
|
||||||
clientStore.fetchClients({ page: 1, per_page: props.pagination.per_page, search: query });
|
clientStore.fetchClients({
|
||||||
|
page: 1,
|
||||||
|
per_page: props.pagination.per_page,
|
||||||
|
search: query,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,5 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<fournisseur-detail-template>
|
<fournisseur-detail-template>
|
||||||
|
<template #header-right>
|
||||||
|
<span class="badge bg-white text-primary px-3 py-2">
|
||||||
|
{{ fournisseur.type_label || "Fournisseur" }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="badge px-3 py-2"
|
||||||
|
:class="
|
||||||
|
fournisseur.is_active
|
||||||
|
? 'bg-white text-success'
|
||||||
|
: 'bg-white text-danger'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ fournisseur.is_active ? "Actif" : "Inactif" }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #summary-cards>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Contacts</p>
|
||||||
|
<h5 class="mb-0">{{ filteredContactsCount }}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Localisations</p>
|
||||||
|
<h5 class="mb-0">{{ locations.length }}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Email</p>
|
||||||
|
<h6
|
||||||
|
class="mb-0 text-truncate"
|
||||||
|
:title="fournisseur.email || 'Non renseigné'"
|
||||||
|
>
|
||||||
|
{{ fournisseur.email || "Non renseigné" }}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Téléphone</p>
|
||||||
|
<h6 class="mb-0">{{ fournisseur.phone || "Non renseigné" }}</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #button-return>
|
<template #button-return>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<router-link
|
<router-link
|
||||||
@ -204,3 +260,9 @@ const triggerFileInput = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stat-card {
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<fournisseur-template>
|
<fournisseur-template>
|
||||||
|
|
||||||
|
|
||||||
|
<template #summary-cards>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Fournisseurs</p>
|
||||||
|
<h5 class="mb-0">{{ totalFournisseurs }}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Actifs</p>
|
||||||
|
<h5 class="mb-0 text-success">{{ activeFournisseurs }}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card stat-card border-0">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<p class="text-sm text-secondary mb-1">Inactifs</p>
|
||||||
|
<h5 class="mb-0 text-danger">{{ inactiveFournisseurs }}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #fournisseur-new-action>
|
<template #fournisseur-new-action>
|
||||||
<add-button text="Ajouter" @click="goToFournisseur" />
|
<add-button text="Ajouter" @click="goToFournisseur" />
|
||||||
</template>
|
</template>
|
||||||
@ -25,14 +54,14 @@ import FournisseurTable from "@/components/molecules/Tables/CRM/FournisseurTable
|
|||||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { computed, defineProps, defineEmits } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const emit = defineEmits(["pushDetails", "deleteFournisseur"]);
|
const emit = defineEmits(["pushDetails", "deleteFournisseur"]);
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
fournisseurData: {
|
fournisseurData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: [],
|
default: [],
|
||||||
@ -43,6 +72,17 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalFournisseurs = computed(() => props.fournisseurData?.length || 0);
|
||||||
|
const activeFournisseurs = computed(
|
||||||
|
() => props.fournisseurData?.filter((f) => f?.is_active).length || 0
|
||||||
|
);
|
||||||
|
const inactiveFournisseurs = computed(
|
||||||
|
() => totalFournisseurs.value - activeFournisseurs.value
|
||||||
|
);
|
||||||
|
const fournisseursWithEmail = computed(
|
||||||
|
() => props.fournisseurData?.filter((f) => !!f?.email).length || 0
|
||||||
|
);
|
||||||
|
|
||||||
const goToFournisseur = () => {
|
const goToFournisseur = () => {
|
||||||
router.push({
|
router.push({
|
||||||
name: "Creation fournisseur",
|
name: "Creation fournisseur",
|
||||||
@ -62,3 +102,9 @@ const deleteFournisseur = (fournisseur) => {
|
|||||||
emit("deleteFournisseur", fournisseur);
|
emit("deleteFournisseur", fournisseur);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stat-card {
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -11,12 +11,14 @@
|
|||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="addChildModalLabel">Ajouter un sous-client</h5>
|
<h5 id="addChildModalLabel" class="modal-title">
|
||||||
|
Ajouter un sous-client
|
||||||
|
</h5>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-close text-dark"
|
class="btn-close text-dark"
|
||||||
@click="closeModal"
|
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
@click="closeModal"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
@ -24,7 +26,10 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Rechercher un client</label>
|
<label class="form-label">Rechercher un client</label>
|
||||||
<div v-if="selectedClient" class="d-flex align-items-center justify-content-between p-2 border rounded mb-3 bg-light">
|
<div
|
||||||
|
v-if="selectedClient"
|
||||||
|
class="d-flex align-items-center justify-content-between p-2 border rounded mb-3 bg-light"
|
||||||
|
>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-avatar
|
<soft-avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -33,11 +38,18 @@
|
|||||||
alt="selected client"
|
alt="selected client"
|
||||||
/>
|
/>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<span class="font-weight-bold">{{ selectedClient.name }}</span>
|
<span class="font-weight-bold">{{
|
||||||
<span class="text-xs text-muted">{{ selectedClient.email }}</span>
|
selectedClient.name
|
||||||
|
}}</span>
|
||||||
|
<span class="text-xs text-muted">{{
|
||||||
|
selectedClient.email
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-link text-danger mb-0" @click="selectedClient = null">
|
<button
|
||||||
|
class="btn btn-link text-danger mb-0"
|
||||||
|
@click="selectedClient = null"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -50,14 +62,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary btn-sm" @click="closeModal">Annuler</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-success btn-sm"
|
class="btn btn-success btn-sm"
|
||||||
:disabled="!selectedClient || loading"
|
:disabled="!selectedClient || loading"
|
||||||
@click="confirmAdd"
|
@click="confirmAdd"
|
||||||
>
|
>
|
||||||
<span v-if="loading" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
<span
|
||||||
|
v-if="loading"
|
||||||
|
class="spinner-border spinner-border-sm me-1"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
Ajouter
|
Ajouter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +103,7 @@ const props = defineProps({
|
|||||||
excludeIds: {
|
excludeIds: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["close", "add"]);
|
const emit = defineEmits(["close", "add"]);
|
||||||
|
|||||||
@ -23,7 +23,11 @@
|
|||||||
>
|
>
|
||||||
Retour
|
Retour
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button color="info" variant="gradient" @click="handleEdit">
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="gradient"
|
||||||
|
@click="handleEdit"
|
||||||
|
>
|
||||||
Modifier
|
Modifier
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
@ -42,7 +46,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<h6 class="text-sm text-uppercase text-muted">Date de création</h6>
|
<h6 class="text-sm text-uppercase text-muted">
|
||||||
|
Date de création
|
||||||
|
</h6>
|
||||||
<p class="text-sm">{{ formatDate(clientGroup.created_at) }}</p>
|
<p class="text-sm">{{ formatDate(clientGroup.created_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
|
|||||||
@ -7,7 +7,68 @@
|
|||||||
<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">
|
||||||
|
<div class="odoo-toolbar">
|
||||||
|
<div class="statusbar-wrapper">
|
||||||
|
<button
|
||||||
|
v-for="status in availableStatuses"
|
||||||
|
:key="status"
|
||||||
|
type="button"
|
||||||
|
class="status-step"
|
||||||
|
:class="{ active: status === commande.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="!primaryNextStatus || isUpdatingStatus"
|
||||||
|
@click="handlePrimaryAction"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check me-2"></i>
|
||||||
|
{{ primaryActionLabel }}
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
:color="secondaryActionColor"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
:disabled="!secondaryActionTargetStatus || isUpdatingStatus"
|
||||||
|
@click="handleSecondaryAction"
|
||||||
|
>
|
||||||
|
<i :class="`${secondaryActionIcon} me-2`"></i>
|
||||||
|
{{ secondaryActionLabel }}
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="dark"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-toolbar btn-sm"
|
||||||
|
@click="sendByEmail"
|
||||||
|
>
|
||||||
|
<i class="fas fa-paper-plane me-2"></i>
|
||||||
|
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 -->
|
<!-- Header Section -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
@ -49,7 +110,9 @@
|
|||||||
<!-- Row 3: Adresse livraison -->
|
<!-- Row 3: Adresse livraison -->
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
<label class="form-label">Adresse livraison</label>
|
<label class="form-label">Adresse livraison</label>
|
||||||
<div class="info-value">{{ commande.deliveryAddress || 'Non spécifiée' }}</div>
|
<div class="info-value">
|
||||||
|
{{ commande.deliveryAddress || "Non spécifiée" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -63,11 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lines-container">
|
<div class="lines-container">
|
||||||
<div
|
<div v-for="line in commande.lines" :key="line.id" class="line-item">
|
||||||
v-for="line in commande.lines"
|
|
||||||
:key="line.id"
|
|
||||||
class="line-item"
|
|
||||||
>
|
|
||||||
<div class="row g-2 align-items-center">
|
<div class="row g-2 align-items-center">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<label class="form-label text-xs">Désignation</label>
|
<label class="form-label text-xs">Désignation</label>
|
||||||
@ -81,7 +140,9 @@
|
|||||||
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<label class="form-label text-xs">Prix HT</label>
|
<label class="form-label text-xs">Prix HT</label>
|
||||||
<div class="line-price">{{ formatCurrency(line.price_ht) }}</div>
|
<div class="line-price">
|
||||||
|
{{ formatCurrency(line.price_ht) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3 d-flex flex-column align-items-end">
|
<div class="col-md-3 d-flex flex-column align-items-end">
|
||||||
@ -100,15 +161,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(commande.total_ht) }}</span>
|
<span class="total-value">{{
|
||||||
|
formatCurrency(commande.total_ht)
|
||||||
|
}}</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(commande.total_tva) }}</span>
|
<span class="total-value">{{
|
||||||
|
formatCurrency(commande.total_tva)
|
||||||
|
}}</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(commande.total_ttc) }}</span>
|
<span class="total-amount">{{
|
||||||
|
formatCurrency(commande.total_ttc)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,49 +203,12 @@
|
|||||||
<div class="info-value notes-content">{{ commande.notes }}</div>
|
<div class="info-value notes-content">{{ commande.notes }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="action-buttons">
|
|
||||||
<div class="position-relative d-inline-block">
|
|
||||||
<soft-button
|
|
||||||
color="secondary"
|
|
||||||
variant="gradient"
|
|
||||||
@click="dropdownOpen = !dropdownOpen"
|
|
||||||
class="btn-status"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exchange-alt me-2"></i>
|
|
||||||
Changer le statut
|
|
||||||
<i class="fas fa-chevron-down ms-2"></i>
|
|
||||||
</soft-button>
|
|
||||||
<ul
|
|
||||||
v-if="dropdownOpen"
|
|
||||||
class="dropdown-menu show position-absolute"
|
|
||||||
style="top: 100%; left: 0; z-index: 1000;"
|
|
||||||
>
|
|
||||||
<li v-for="status in availableStatuses" :key="status">
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
:class="{ active: status === commande.status }"
|
|
||||||
href="javascript:;"
|
|
||||||
@click="changeStatus(status); dropdownOpen = false;"
|
|
||||||
>
|
|
||||||
<i :class="getStatusIcon(status) + ' me-2'"></i>
|
|
||||||
{{ getStatusLabel(status) }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<soft-button color="info" variant="outline" class="btn-pdf">
|
|
||||||
<i class="fas fa-file-pdf me-2"></i> Télécharger PDF
|
|
||||||
</soft-button>
|
|
||||||
</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,7 +339,9 @@ 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
|
||||||
@ -269,19 +358,22 @@ const fetchCommande = async () => {
|
|||||||
|
|
||||||
// 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,18 +385,62 @@ 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(() => {
|
||||||
@ -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,23 +729,6 @@ 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 {
|
||||||
@ -550,14 +745,22 @@ onMounted(() => {
|
|||||||
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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -38,7 +38,12 @@
|
|||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
<soft-button color="secondary" variant="outline" size="sm" @click="handleBack">
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="handleBack"
|
||||||
|
>
|
||||||
Retour
|
Retour
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button color="info" size="sm">
|
<soft-button color="info" size="sm">
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const currentFilter = ref(null);
|
|||||||
|
|
||||||
const filteredFactures = computed(() => {
|
const filteredFactures = computed(() => {
|
||||||
if (!currentFilter.value) return factures.value;
|
if (!currentFilter.value) return factures.value;
|
||||||
return factures.value.filter(f => f.status === currentFilter.value);
|
return factures.value.filter((f) => f.status === currentFilter.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
|
|||||||
@ -5,7 +5,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header pb-0">
|
<div class="card-header pb-0">
|
||||||
<h5 class="mb-0">Nouvelle Facture Fournisseur</h5>
|
<h5 class="mb-0">Nouvelle Facture Fournisseur</h5>
|
||||||
<p class="text-sm mb-0">Saisissez les informations de la facture reçue.</p>
|
<p class="text-sm mb-0">
|
||||||
|
Saisissez les informations de la facture reçue.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<new-facture-fournisseur-form
|
<new-facture-fournisseur-form
|
||||||
|
|||||||
@ -9,9 +9,7 @@
|
|||||||
<small class="text-muted">Communiquez avec votre équipe</small>
|
<small class="text-muted">Communiquez avec votre équipe</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="badge bg-info">
|
<span class="badge bg-info"> {{ unreadCount }} non lu(s) </span>
|
||||||
{{ unreadCount }} non lu(s)
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -22,19 +20,21 @@
|
|||||||
<button
|
<button
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:class="{ active: activeTab === 'inbox' }"
|
:class="{ active: activeTab === 'inbox' }"
|
||||||
@click="activeTab = 'inbox'"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
@click="activeTab = 'inbox'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-inbox"></i> Réception
|
<i class="fas fa-inbox"></i> Réception
|
||||||
<span class="badge bg-danger ms-2" v-if="unreadCount > 0">{{ unreadCount }}</span>
|
<span v-if="unreadCount > 0" class="badge bg-danger ms-2">{{
|
||||||
|
unreadCount
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button
|
<button
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:class="{ active: activeTab === 'compose' }"
|
:class="{ active: activeTab === 'compose' }"
|
||||||
@click="activeTab = 'compose'"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
@click="activeTab = 'compose'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-pen"></i> Nouveau message
|
<i class="fas fa-pen"></i> Nouveau message
|
||||||
</button>
|
</button>
|
||||||
@ -43,8 +43,8 @@
|
|||||||
<button
|
<button
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:class="{ active: activeTab === 'sent' }"
|
:class="{ active: activeTab === 'sent' }"
|
||||||
@click="activeTab = 'sent'"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
@click="activeTab = 'sent'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-paper-plane"></i> Envoyés
|
<i class="fas fa-paper-plane"></i> Envoyés
|
||||||
</button>
|
</button>
|
||||||
@ -72,17 +72,10 @@
|
|||||||
@form-data-change="updateNewMessage"
|
@form-data-change="updateNewMessage"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||||
<soft-button
|
<soft-button color="secondary" variant="outline" @click="resetForm">
|
||||||
color="secondary"
|
|
||||||
variant="outline"
|
|
||||||
@click="resetForm"
|
|
||||||
>
|
|
||||||
<i class="fas fa-redo"></i> Réinitialiser
|
<i class="fas fa-redo"></i> Réinitialiser
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button
|
<soft-button color="success" @click="sendMessage">
|
||||||
color="success"
|
|
||||||
@click="sendMessage"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check"></i> Envoyer
|
<i class="fas fa-check"></i> Envoyer
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
@ -260,7 +253,9 @@ const sendMessage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = users.value.find((u) => u.id === parseInt(newMessage.value.recipientId));
|
const recipient = users.value.find(
|
||||||
|
(u) => u.id === parseInt(newMessage.value.recipientId)
|
||||||
|
);
|
||||||
|
|
||||||
const sentMessage = {
|
const sentMessage = {
|
||||||
id: sent.value.length + 101,
|
id: sent.value.length + 101,
|
||||||
|
|||||||
@ -6,7 +6,10 @@
|
|||||||
</soft-button>
|
</soft-button>
|
||||||
</template>
|
</template>
|
||||||
<template #header-pagination>
|
<template #header-pagination>
|
||||||
<div class="d-flex justify-content-center" v-if="pagination && pagination.last_page > 1">
|
<div
|
||||||
|
v-if="pagination && pagination.last_page > 1"
|
||||||
|
class="d-flex justify-content-center"
|
||||||
|
>
|
||||||
<soft-pagination color="success" size="sm">
|
<soft-pagination color="success" size="sm">
|
||||||
<soft-pagination-item
|
<soft-pagination-item
|
||||||
prev
|
prev
|
||||||
@ -31,12 +34,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #select-filter>
|
<template #select-filter>
|
||||||
<soft-button color="dark" variant="outline" class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<soft-button
|
||||||
|
color="dark"
|
||||||
|
variant="outline"
|
||||||
|
class="dropdown-toggle"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
<i class="fas fa-filter me-2"></i> Filtrer
|
<i class="fas fa-filter me-2"></i> Filtrer
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<ul class="dropdown-menu dropdown-menu-lg-start px-2 py-3">
|
<ul class="dropdown-menu dropdown-menu-lg-start px-2 py-3">
|
||||||
<li><a class="dropdown-item border-radius-md" href="javascript:;">Par date</a></li>
|
<li>
|
||||||
<li><a class="dropdown-item border-radius-md" href="javascript:;">Par statut</a></li>
|
<a class="dropdown-item border-radius-md" href="javascript:;"
|
||||||
|
>Par date</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item border-radius-md" href="javascript:;"
|
||||||
|
>Par statut</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<template #intervention-other-action>
|
<template #intervention-other-action>
|
||||||
@ -104,7 +121,7 @@ const props = defineProps({
|
|||||||
pagination: {
|
pagination: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
@ -115,8 +132,9 @@ const go = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changePage = (page) => {
|
const changePage = (page) => {
|
||||||
if (typeof page !== 'number') return;
|
if (typeof page !== "number") return;
|
||||||
if (page < 1 || (props.pagination && page > props.pagination.last_page)) return;
|
if (page < 1 || (props.pagination && page > props.pagination.last_page))
|
||||||
|
return;
|
||||||
if (page === props.pagination.current_page) return;
|
if (page === props.pagination.current_page) return;
|
||||||
emit("page-change", page);
|
emit("page-change", page);
|
||||||
};
|
};
|
||||||
@ -126,7 +144,11 @@ const visiblePages = computed(() => {
|
|||||||
const { current_page, last_page } = props.pagination;
|
const { current_page, last_page } = props.pagination;
|
||||||
const delta = 2;
|
const delta = 2;
|
||||||
const range = [];
|
const range = [];
|
||||||
for (let i = Math.max(2, current_page - delta); i <= Math.min(last_page - 1, current_page + delta); i++) {
|
for (
|
||||||
|
let i = Math.max(2, current_page - delta);
|
||||||
|
i <= Math.min(last_page - 1, current_page + delta);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
range.push(i);
|
range.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,18 +195,24 @@ const visiblePages = computed(() => {
|
|||||||
for (let i = 1; i <= total; i++) p.push(i);
|
for (let i = 1; i <= total; i++) p.push(i);
|
||||||
} else {
|
} else {
|
||||||
p.push(1);
|
p.push(1);
|
||||||
if (current_page > 3) p.push('...');
|
if (current_page > 3) p.push("...");
|
||||||
|
|
||||||
let midStart = Math.max(2, current_page - 1);
|
let midStart = Math.max(2, current_page - 1);
|
||||||
let midEnd = Math.min(total - 1, current_page + 1);
|
let midEnd = Math.min(total - 1, current_page + 1);
|
||||||
|
|
||||||
// pinned logic adjustment
|
// pinned logic adjustment
|
||||||
if (current_page < 4) { midStart = 2; midEnd = 4; }
|
if (current_page < 4) {
|
||||||
if (current_page > total - 3) { midStart = total - 3; midEnd = total - 1; }
|
midStart = 2;
|
||||||
|
midEnd = 4;
|
||||||
|
}
|
||||||
|
if (current_page > total - 3) {
|
||||||
|
midStart = total - 3;
|
||||||
|
midEnd = total - 1;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = midStart; i <= midEnd; i++) p.push(i);
|
for (let i = midStart; i <= midEnd; i++) p.push(i);
|
||||||
|
|
||||||
if (current_page < total - 2) p.push('...');
|
if (current_page < total - 2) p.push("...");
|
||||||
p.push(total);
|
p.push(total);
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
|
|||||||
@ -23,7 +23,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<h6 class="mb-3 text-sm">Historique</h6>
|
<h6 class="mb-3 text-sm">Historique</h6>
|
||||||
<div v-if="invoice.history && invoice.history.length > 0">
|
<div v-if="invoice.history && invoice.history.length > 0">
|
||||||
<div v-for="(entry, index) in invoice.history" :key="index" class="mb-2">
|
<div
|
||||||
|
v-for="(entry, index) in invoice.history"
|
||||||
|
:key="index"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
<span class="text-xs text-secondary">
|
<span class="text-xs text-secondary">
|
||||||
{{ formatDate(entry.changed_at) }}
|
{{ formatDate(entry.changed_at) }}
|
||||||
</span>
|
</span>
|
||||||
@ -38,13 +42,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h6 class="mb-3 text-sm">Informations Client</h6>
|
<h6 class="mb-3 text-sm">Informations Client</h6>
|
||||||
<p class="text-sm mb-1">
|
<p class="text-sm mb-1">
|
||||||
<strong>{{ invoice.client ? invoice.client.name : 'Client inconnu' }}</strong>
|
<strong>{{
|
||||||
|
invoice.client ? invoice.client.name : "Client inconnu"
|
||||||
|
}}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-secondary mb-1">
|
<p class="text-xs text-secondary mb-1">
|
||||||
{{ invoice.client ? invoice.client.email : '' }}
|
{{ invoice.client ? invoice.client.email : "" }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-secondary mb-0">
|
<p class="text-xs text-secondary mb-0">
|
||||||
{{ invoice.client ? invoice.client.phone : '' }}
|
{{ invoice.client ? invoice.client.phone : "" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -71,14 +77,17 @@
|
|||||||
<ul
|
<ul
|
||||||
v-if="dropdownOpen"
|
v-if="dropdownOpen"
|
||||||
class="dropdown-menu show position-absolute"
|
class="dropdown-menu show position-absolute"
|
||||||
style="top: 100%; left: 0; z-index: 1000;"
|
style="top: 100%; left: 0; z-index: 1000"
|
||||||
>
|
>
|
||||||
<li v-for="status in availableStatuses" :key="status">
|
<li v-for="status in availableStatuses" :key="status">
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
:class="{ active: status === invoice.status }"
|
:class="{ active: status === invoice.status }"
|
||||||
href="javascript:;"
|
href="javascript:;"
|
||||||
@click="changeStatus(status); dropdownOpen = false;"
|
@click="
|
||||||
|
changeStatus(status);
|
||||||
|
dropdownOpen = false;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ getStatusLabel(status) }}
|
{{ getStatusLabel(status) }}
|
||||||
</a>
|
</a>
|
||||||
@ -180,7 +189,7 @@ const changeStatus = async (newStatus) => {
|
|||||||
invoice.value = updated;
|
invoice.value = updated;
|
||||||
|
|
||||||
notificationStore.success(
|
notificationStore.success(
|
||||||
'Statut mis à jour',
|
"Statut mis à jour",
|
||||||
`La facture est maintenant "${getStatusLabel(newStatus)}"`,
|
`La facture est maintenant "${getStatusLabel(newStatus)}"`,
|
||||||
3000
|
3000
|
||||||
);
|
);
|
||||||
@ -188,8 +197,8 @@ const changeStatus = async (newStatus) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to update status", e);
|
console.error("Failed to update status", e);
|
||||||
notificationStore.error(
|
notificationStore.error(
|
||||||
'Erreur',
|
"Erreur",
|
||||||
'Impossible de mettre à jour le statut',
|
"Impossible de mettre à jour le statut",
|
||||||
3000
|
3000
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
:class="{ show: show, 'd-block': show }"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="planningNewRequestModalLabel"
|
||||||
|
:aria-hidden="!show"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 id="planningNewRequestModalLabel" class="modal-title">Nouvelle demande</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" @click="$emit('close')"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p v-if="!creationType" class="text-sm text-muted mb-3">Choisissez le type à créer :</p>
|
||||||
|
<p v-else class="text-sm text-muted mb-3">{{ creationTypeTitle }}</p>
|
||||||
|
|
||||||
|
<planning-creation-type-selector
|
||||||
|
v-if="!creationType"
|
||||||
|
@select-type="$emit('select-type', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<planning-leave-request-form
|
||||||
|
v-else-if="creationType === 'leave'"
|
||||||
|
:form="leaveForm"
|
||||||
|
:collaborators="collaborators"
|
||||||
|
@update:form="$emit('update:leave-form', $event)"
|
||||||
|
@submit="$emit('submit-leave')"
|
||||||
|
@back="$emit('reset-type')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<planning-event-form
|
||||||
|
v-else-if="creationType === 'event'"
|
||||||
|
:form="eventForm"
|
||||||
|
@update:form="$emit('update:event-form', $event)"
|
||||||
|
@submit="$emit('submit-event')"
|
||||||
|
@back="$emit('reset-type')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="show" class="modal-backdrop fade show" @click="$emit('close')"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import PlanningCreationTypeSelector from "@/components/molecules/Planning/PlanningCreationTypeSelector.vue";
|
||||||
|
import PlanningLeaveRequestForm from "@/components/molecules/Planning/PlanningLeaveRequestForm.vue";
|
||||||
|
import PlanningEventForm from "@/components/molecules/Planning/PlanningEventForm.vue";
|
||||||
|
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
creationType: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
creationTypeTitle: {
|
||||||
|
type: String,
|
||||||
|
default: "Nouvelle demande",
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
leaveForm: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
eventForm: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits([
|
||||||
|
"close",
|
||||||
|
"select-type",
|
||||||
|
"reset-type",
|
||||||
|
"submit-leave",
|
||||||
|
"submit-event",
|
||||||
|
"update:leave-form",
|
||||||
|
"update:event-form",
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<planning-template>
|
<planning-template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="d-flex flex-column gap-3">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<div>
|
<p class="text-sm text-secondary mb-0">
|
||||||
<h1 class="display-4 font-bold bg-gradient-indigo-text mb-1">Planning</h1>
|
{{ interventionCount }} intervention(s) (mes tâches)
|
||||||
<p class="text-sm text-secondary">{{ interventionCount }} intervention(s) (mes tâches)</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Date Navigator Mobile Position could go here if needed, but keeping simple for now -->
|
<div
|
||||||
</div>
|
class="d-flex flex-column flex-md-row gap-2 w-100 w-md-auto align-items-center justify-content-md-end"
|
||||||
<div class="d-flex flex-column flex-md-row gap-3 w-100 w-md-auto align-items-center">
|
>
|
||||||
<!-- Date Navigator -->
|
<!-- Date Navigator -->
|
||||||
<planning-date-navigator
|
<planning-date-navigator
|
||||||
:current-date="currentDate"
|
:current-date="currentDate"
|
||||||
@ -16,16 +16,26 @@
|
|||||||
@next-week="$emit('next-week')"
|
@next-week="$emit('next-week')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 flex-wrap justify-content-end">
|
||||||
<planning-action-button variant="secondary" size="sm" @click="$emit('refresh')">
|
<soft-button
|
||||||
<template #icon><i class="fas fa-sync-alt"></i></template>
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sync-alt me-1"></i>
|
||||||
<span class="d-none d-lg-inline">Actualiser</span>
|
<span class="d-none d-lg-inline">Actualiser</span>
|
||||||
</planning-action-button>
|
</soft-button>
|
||||||
<planning-action-button variant="primary" size="sm" @click="$emit('new-request')">
|
<soft-button
|
||||||
<template #icon><i class="fas fa-plus"></i></template>
|
color="info"
|
||||||
|
variant="gradient"
|
||||||
|
size="sm"
|
||||||
|
@click="$emit('new-request')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus me-1"></i>
|
||||||
<span class="d-none d-lg-inline">Nouvelle demande</span>
|
<span class="d-none d-lg-inline">Nouvelle demande</span>
|
||||||
<span class="d-lg-none">Nouveau</span>
|
<span class="d-lg-none">Nouveau</span>
|
||||||
</planning-action-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -76,7 +86,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, defineProps, defineEmits } from "vue";
|
import { ref, watch, defineProps, defineEmits } from "vue";
|
||||||
import PlanningTemplate from "@/components/templates/Planning/PlanningTemplate.vue";
|
import PlanningTemplate from "@/components/templates/Planning/PlanningTemplate.vue";
|
||||||
import PlanningActionButton from "@/components/atoms/Planning/PlanningActionButton.vue";
|
|
||||||
import PlanningViewToggles from "@/components/molecules/Planning/PlanningViewToggles.vue";
|
import PlanningViewToggles from "@/components/molecules/Planning/PlanningViewToggles.vue";
|
||||||
import PlanningLegend from "@/components/molecules/Planning/PlanningLegend.vue";
|
import PlanningLegend from "@/components/molecules/Planning/PlanningLegend.vue";
|
||||||
import PlanningCollaboratorsSidebar from "@/components/molecules/Planning/PlanningCollaboratorsSidebar.vue";
|
import PlanningCollaboratorsSidebar from "@/components/molecules/Planning/PlanningCollaboratorsSidebar.vue";
|
||||||
@ -84,28 +93,29 @@ import PlanningWeekGrid from "@/components/molecules/Planning/PlanningWeekGrid.v
|
|||||||
import PlanningList from "@/components/molecules/Planning/PlanningList.vue";
|
import PlanningList from "@/components/molecules/Planning/PlanningList.vue";
|
||||||
import PlanningKanban from "@/components/molecules/Planning/PlanningKanban.vue";
|
import PlanningKanban from "@/components/molecules/Planning/PlanningKanban.vue";
|
||||||
import PlanningDateNavigator from "@/components/molecules/Planning/PlanningDateNavigator.vue";
|
import PlanningDateNavigator from "@/components/molecules/Planning/PlanningDateNavigator.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
interventionCount: {
|
interventionCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0,
|
||||||
},
|
},
|
||||||
collaborators: {
|
collaborators: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
interventions: {
|
interventions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
currentDate: {
|
currentDate: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: () => new Date()
|
default: () => new Date(),
|
||||||
},
|
},
|
||||||
activeView: {
|
activeView: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "grille"
|
default: "grille",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@ -116,18 +126,24 @@ const emit = defineEmits([
|
|||||||
"prev-week",
|
"prev-week",
|
||||||
"next-week",
|
"next-week",
|
||||||
"edit-intervention",
|
"edit-intervention",
|
||||||
"update-status"
|
"update-status",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const localActiveView = ref(props.activeView);
|
const localActiveView = ref(props.activeView);
|
||||||
|
|
||||||
watch(() => props.activeView, (newVal) => {
|
watch(
|
||||||
|
() => props.activeView,
|
||||||
|
(newVal) => {
|
||||||
localActiveView.value = newVal;
|
localActiveView.value = newVal;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(() => localActiveView.value, (newVal) => {
|
watch(
|
||||||
|
() => localActiveView.value,
|
||||||
|
(newVal) => {
|
||||||
emit("update:activeView", newVal);
|
emit("update:activeView", newVal);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleCellClick = (info) => {
|
const handleCellClick = (info) => {
|
||||||
emit("cell-click", info);
|
emit("cell-click", info);
|
||||||
@ -143,28 +159,6 @@ const handleUpdateStatus = (payload) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.font-bold {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gradient-indigo-text {
|
|
||||||
background: linear-gradient(to right, #2563eb, #4f46e5, #9333ea);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-4 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.display-4 {
|
|
||||||
font-size: 1.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-secondary {
|
.text-secondary {
|
||||||
color: #64748b !important;
|
color: #64748b !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,14 +54,17 @@
|
|||||||
<ul
|
<ul
|
||||||
v-if="dropdownOpen"
|
v-if="dropdownOpen"
|
||||||
class="dropdown-menu show position-absolute"
|
class="dropdown-menu show position-absolute"
|
||||||
style="top: 100%; left: 0; z-index: 1000;"
|
style="top: 100%; left: 0; z-index: 1000"
|
||||||
>
|
>
|
||||||
<li v-for="status in availableStatuses" :key="status">
|
<li v-for="status in availableStatuses" :key="status">
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
:class="{ active: status === quote.status }"
|
:class="{ active: status === quote.status }"
|
||||||
href="javascript:;"
|
href="javascript:;"
|
||||||
@click="changeStatus(status); dropdownOpen = false;"
|
@click="
|
||||||
|
changeStatus(status);
|
||||||
|
dropdownOpen = false;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ getStatusLabel(status) }}
|
{{ getStatusLabel(status) }}
|
||||||
</a>
|
</a>
|
||||||
@ -164,7 +167,7 @@ const changeStatus = async (newStatus) => {
|
|||||||
|
|
||||||
// Show success notification
|
// Show success notification
|
||||||
notificationStore.success(
|
notificationStore.success(
|
||||||
'Statut mis à jour',
|
"Statut mis à jour",
|
||||||
`Le devis est maintenant "${getStatusLabel(newStatus)}"`,
|
`Le devis est maintenant "${getStatusLabel(newStatus)}"`,
|
||||||
3000
|
3000
|
||||||
);
|
);
|
||||||
@ -172,8 +175,8 @@ const changeStatus = async (newStatus) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to update status", e);
|
console.error("Failed to update status", e);
|
||||||
notificationStore.error(
|
notificationStore.error(
|
||||||
'Erreur',
|
"Erreur",
|
||||||
'Impossible de mettre à jour le statut',
|
"Impossible de mettre à jour le statut",
|
||||||
3000
|
3000
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -113,9 +113,7 @@ const practitioners = ref([
|
|||||||
const applyFilter = (filterData) => {
|
const applyFilter = (filterData) => {
|
||||||
filterStartDate.value = filterData.startDate;
|
filterStartDate.value = filterData.startDate;
|
||||||
filterEndDate.value = filterData.endDate;
|
filterEndDate.value = filterData.endDate;
|
||||||
alert(
|
alert(`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`);
|
||||||
`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportPDF = () => {
|
const exportPDF = () => {
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -15,8 +15,20 @@
|
|||||||
<div
|
<div
|
||||||
class="text-right col-lg-6 d-flex flex-column justify-content-center"
|
class="text-right col-lg-6 d-flex flex-column justify-content-center"
|
||||||
>
|
>
|
||||||
<div class="ms-lg-auto me-lg-0 me-auto mt-lg-0">
|
<div class="w-100 mt-lg-0">
|
||||||
<div v-if="!isEditMode" class="d-flex gap-2">
|
<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="goBack"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>
|
||||||
|
Retour à la liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 ms-auto">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="mt-2 mb-0 btn bg-gradient-info"
|
class="mt-2 mb-0 btn bg-gradient-info"
|
||||||
@ -35,6 +47,7 @@
|
|||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="d-flex gap-2">
|
<div v-else class="d-flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -100,9 +113,91 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product details/Edit form -->
|
<!-- Product details/Edit form -->
|
||||||
<div v-else-if="productData" class="product-details-content">
|
<div v-else-if="productData" class="product-details-content mt-3">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-xl-3 col-lg-4">
|
||||||
|
<div class="card product-side-nav-card">
|
||||||
|
<div class="card-body pb-2">
|
||||||
|
<div class="product-sidebar-profile">
|
||||||
|
<div class="product-sidebar-image">
|
||||||
|
<product-image
|
||||||
|
:image-url="productData.media?.photo_url"
|
||||||
|
:alt-text="`Image de ${productData.nom}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h6 class="product-sidebar-title text-center mb-1">
|
||||||
|
{{ productData.nom }}
|
||||||
|
</h6>
|
||||||
|
<p class="product-sidebar-reference text-center mb-0">
|
||||||
|
{{ productData.reference || "Sans référence" }}
|
||||||
|
</p>
|
||||||
|
<div class="product-sidebar-badges mt-3">
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
:class="
|
||||||
|
productData.is_low_stock
|
||||||
|
? 'bg-warning text-dark'
|
||||||
|
: 'bg-success'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ productData.is_low_stock ? "Stock faible" : "Stock OK" }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isExpired" class="badge bg-danger">Expiré</span>
|
||||||
|
<span
|
||||||
|
v-else-if="isExpiringSoon"
|
||||||
|
class="badge bg-info text-dark"
|
||||||
|
>Expire bientôt</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="horizontal dark my-2 mx-3" />
|
||||||
|
|
||||||
|
<div class="card-body pt-2">
|
||||||
|
<ul class="nav nav-pills flex-column product-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">Détails</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item pt-2">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ active: activeTab === 'stock' }"
|
||||||
|
href="javascript:;"
|
||||||
|
@click="activeTab = 'stock'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-boxes me-2"></i>
|
||||||
|
<span class="text-sm">Stock</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 de stock</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-9 col-lg-8">
|
||||||
|
<div v-show="activeTab === 'details'">
|
||||||
<!-- Product Header Card -->
|
<!-- Product Header Card -->
|
||||||
<div class="mt-4 card">
|
<div class="card">
|
||||||
<div class="card-header bg-gradient-primary text-white">
|
<div class="card-header bg-gradient-primary text-white">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -164,7 +259,9 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Nom du produit"
|
placeholder="Nom du produit"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static">{{ productData.nom }}</p>
|
<p v-else class="form-control-static">
|
||||||
|
{{ productData.nom }}
|
||||||
|
</p>
|
||||||
<div v-if="validationErrors.nom" class="invalid-feedback">
|
<div v-if="validationErrors.nom" class="invalid-feedback">
|
||||||
{{ validationErrors.nom[0] }}
|
{{ validationErrors.nom[0] }}
|
||||||
</div>
|
</div>
|
||||||
@ -182,7 +279,10 @@
|
|||||||
<p v-else class="form-control-static">
|
<p v-else class="form-control-static">
|
||||||
{{ productData.reference }}
|
{{ productData.reference }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="validationErrors.reference" class="invalid-feedback">
|
<div
|
||||||
|
v-if="validationErrors.reference"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
{{ validationErrors.reference[0] }}
|
{{ validationErrors.reference[0] }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -193,23 +293,27 @@
|
|||||||
<label>Catégorie *</label>
|
<label>Catégorie *</label>
|
||||||
<select
|
<select
|
||||||
v-if="isEditMode"
|
v-if="isEditMode"
|
||||||
v-model="formData.categorie"
|
v-model="formData.categorie_id"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:class="{ 'is-invalid': validationErrors.categorie }"
|
:class="{ 'is-invalid': validationErrors.categorie_id }"
|
||||||
>
|
>
|
||||||
<option value="">Sélectionner une catégorie</option>
|
<option value="">Sélectionner une catégorie</option>
|
||||||
<option value="Furniture">Mobilier</option>
|
<option
|
||||||
<option value="Electronics">Électronique</option>
|
v-for="category in categories"
|
||||||
<option value="Clothing">Vêtements</option>
|
:key="category.id"
|
||||||
<option value="Books">Livres</option>
|
:value="category.id.toString()"
|
||||||
<option value="Food">Nourriture</option>
|
>
|
||||||
<option value="Others">Autres</option>
|
{{ category.name }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<p v-else class="form-control-static">
|
<p v-else class="form-control-static">
|
||||||
{{ productData.categorie || "Non catégorisé" }}
|
{{ displayCategoryName }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="validationErrors.categorie" class="invalid-feedback">
|
<div
|
||||||
{{ validationErrors.categorie[0] }}
|
v-if="validationErrors.categorie_id"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ validationErrors.categorie_id[0] }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -225,7 +329,10 @@
|
|||||||
<p v-else class="form-control-static">
|
<p v-else class="form-control-static">
|
||||||
{{ productData.fabricant || "Non renseigné" }}
|
{{ productData.fabricant || "Non renseigné" }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="validationErrors.fabricant" class="invalid-feedback">
|
<div
|
||||||
|
v-if="validationErrors.fabricant"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
{{ validationErrors.fabricant[0] }}
|
{{ validationErrors.fabricant[0] }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -245,7 +352,10 @@
|
|||||||
<p v-else class="form-control-static">
|
<p v-else class="form-control-static">
|
||||||
{{ productData.numero_lot || "Non renseigné" }}
|
{{ productData.numero_lot || "Non renseigné" }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="validationErrors.numero_lot" class="invalid-feedback">
|
<div
|
||||||
|
v-if="validationErrors.numero_lot"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
{{ validationErrors.numero_lot[0] }}
|
{{ validationErrors.numero_lot[0] }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -255,7 +365,9 @@
|
|||||||
v-if="isEditMode"
|
v-if="isEditMode"
|
||||||
v-model="formData.date_expiration"
|
v-model="formData.date_expiration"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:class="{ 'is-invalid': validationErrors.date_expiration }"
|
:class="{
|
||||||
|
'is-invalid': validationErrors.date_expiration,
|
||||||
|
}"
|
||||||
type="date"
|
type="date"
|
||||||
:min="new Date().toISOString().split('T')[0]"
|
:min="new Date().toISOString().split('T')[0]"
|
||||||
/>
|
/>
|
||||||
@ -286,7 +398,9 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Unité de mesure"
|
placeholder="Unité de mesure"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static">{{ productData.unite }}</p>
|
<p v-else class="form-control-static">
|
||||||
|
{{ productData.unite }}
|
||||||
|
</p>
|
||||||
<div v-if="validationErrors.unite" class="invalid-feedback">
|
<div v-if="validationErrors.unite" class="invalid-feedback">
|
||||||
{{ validationErrors.unite[0] }}
|
{{ validationErrors.unite[0] }}
|
||||||
</div>
|
</div>
|
||||||
@ -308,10 +422,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stock and Pricing Card -->
|
<!-- Conditioning Card -->
|
||||||
<div class="mt-4 card">
|
<div class="mt-4 card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="font-weight-bolder mb-4">Stock et tarification</h5>
|
<h5 class="font-weight-bolder mb-4">Conditionnement</h5>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@ -332,7 +446,8 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
<span class="stock-unit">{{ productData.unite }}</span>
|
<span class="stock-unit">{{ productData.unite }}</span>
|
||||||
<span v-if="productData.stock_minimum" class="stock-min">
|
<span v-if="productData.stock_minimum" class="stock-min">
|
||||||
(Min: {{ productData.stock_minimum }} {{ productData.unite }})
|
(Min: {{ productData.stock_minimum }}
|
||||||
|
{{ productData.unite }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -393,10 +508,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conditioning Card -->
|
<!-- Supplier Card -->
|
||||||
<div class="mt-4 card">
|
<div class="mt-4 card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="font-weight-bolder mb-4">Conditionnement</h5>
|
<h5 class="font-weight-bolder mb-4">Fournisseur</h5>
|
||||||
|
<div v-if="productData.fournisseur" class="supplier-info">
|
||||||
|
<div class="supplier-details">
|
||||||
|
<div class="supplier-main">
|
||||||
|
<span class="supplier-name">{{
|
||||||
|
productData.fournisseur.name ||
|
||||||
|
productData.fournisseur.nom
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="productData.fournisseur.email"
|
||||||
|
class="supplier-email"
|
||||||
|
>
|
||||||
|
{{ productData.fournisseur.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="supplier-action-btn"
|
||||||
|
title="Voir le fournisseur"
|
||||||
|
@click="handleViewSupplier(productData.fournisseur)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-supplier">
|
||||||
|
<span class="no-supplier-text"
|
||||||
|
>Aucun fournisseur associé</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Documents Card -->
|
||||||
|
<div v-if="hasDocuments" class="mt-4 card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="font-weight-bolder mb-4">Documents</h5>
|
||||||
|
<div class="documents-list">
|
||||||
|
<div
|
||||||
|
v-if="productData.media?.fiche_technique_url"
|
||||||
|
class="document-item"
|
||||||
|
>
|
||||||
|
<i class="fas fa-file-pdf document-icon"></i>
|
||||||
|
<div class="document-details">
|
||||||
|
<span class="document-name">Fiche technique</span>
|
||||||
|
<a
|
||||||
|
:href="productData.media.fiche_technique_url"
|
||||||
|
target="_blank"
|
||||||
|
class="document-link"
|
||||||
|
>
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
Télécharger
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="activeTab === 'stock'">
|
||||||
|
<!-- Stock and Pricing Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="font-weight-bolder mb-4">Stock et tarification</h5>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -405,7 +583,9 @@
|
|||||||
v-if="isEditMode"
|
v-if="isEditMode"
|
||||||
v-model="formData.conditionnement_nom"
|
v-model="formData.conditionnement_nom"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:class="{ 'is-invalid': validationErrors.conditionnement_nom }"
|
:class="{
|
||||||
|
'is-invalid': validationErrors.conditionnement_nom,
|
||||||
|
}"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Nom du conditionnement"
|
placeholder="Nom du conditionnement"
|
||||||
/>
|
/>
|
||||||
@ -448,58 +628,80 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Supplier Card -->
|
|
||||||
<div class="mt-4 card">
|
<div class="mt-4 card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="font-weight-bolder mb-4">Fournisseur</h5>
|
<h6 class="mb-3">Indicateurs de stock</h6>
|
||||||
<div v-if="productData.fournisseur" class="supplier-info">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<div class="supplier-details">
|
|
||||||
<div class="supplier-main">
|
|
||||||
<span class="supplier-name">{{
|
|
||||||
productData.fournisseur.name || productData.fournisseur.nom
|
|
||||||
}}</span>
|
|
||||||
<span
|
<span
|
||||||
v-if="productData.fournisseur.email"
|
class="badge"
|
||||||
class="supplier-email"
|
:class="
|
||||||
|
productData.is_low_stock
|
||||||
|
? 'bg-warning text-dark'
|
||||||
|
: 'bg-success'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ productData.fournisseur.email }}
|
{{ productData.is_low_stock ? "Stock faible" : "Stock OK" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span v-if="isExpired" class="badge bg-danger">Expiré</span>
|
||||||
<button
|
<span
|
||||||
class="supplier-action-btn"
|
v-else-if="isExpiringSoon"
|
||||||
title="Voir le fournisseur"
|
class="badge bg-info text-dark"
|
||||||
@click="handleViewSupplier(productData.fournisseur)"
|
>Expire bientôt</span
|
||||||
>
|
>
|
||||||
<i class="fas fa-external-link-alt"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-supplier">
|
|
||||||
<span class="no-supplier-text">Aucun fournisseur associé</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Documents Card -->
|
<div v-show="activeTab === 'movements'">
|
||||||
<div v-if="hasDocuments" class="mt-4 card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="font-weight-bolder mb-4">Documents</h5>
|
<h5 class="font-weight-bolder mb-4">Mouvements de stock</h5>
|
||||||
<div class="documents-list">
|
<div v-if="stockMovements.length === 0" class="text-muted">
|
||||||
<div
|
Aucun mouvement de stock disponible.
|
||||||
v-if="productData.media?.fiche_technique_url"
|
</div>
|
||||||
class="document-item"
|
<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"
|
||||||
>
|
>
|
||||||
<i class="fas fa-file-pdf document-icon"></i>
|
Date
|
||||||
<div class="document-details">
|
</th>
|
||||||
<span class="document-name">Fiche technique</span>
|
<th
|
||||||
<a
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
:href="productData.media.fiche_technique_url"
|
|
||||||
target="_blank"
|
|
||||||
class="document-link"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-download"></i>
|
Type
|
||||||
Télécharger
|
</th>
|
||||||
</a>
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Quantité
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder"
|
||||||
|
>
|
||||||
|
Référence
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="movement in stockMovements" :key="movement.id">
|
||||||
|
<td class="text-sm">
|
||||||
|
{{ movement.date || movement.created_at || "-" }}
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">{{ movement.type || "-" }}</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
{{ movement.quantite ?? movement.quantity ?? "-" }}
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
{{ movement.reference || movement.reason || "-" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -530,12 +732,14 @@ import { ref, onMounted, computed, defineProps, defineEmits } from "vue";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import ProductService from "@/services/product";
|
import ProductService from "@/services/product";
|
||||||
import { useProductStore } from "@/stores/productStore";
|
import { useProductStore } from "@/stores/productStore";
|
||||||
|
import { useProductCategoryStore } from "@/stores/productCategoryStore";
|
||||||
import ProductImage from "@/components/atoms/Product/ProductImage.vue";
|
import ProductImage from "@/components/atoms/Product/ProductImage.vue";
|
||||||
import PriceDisplay from "@/components/atoms/Product/PriceDisplay.vue";
|
import PriceDisplay from "@/components/atoms/Product/PriceDisplay.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const productStore = useProductStore();
|
const productStore = useProductStore();
|
||||||
|
const productCategoryStore = useProductCategoryStore();
|
||||||
|
|
||||||
const emit = defineEmits(["viewSupplier"]);
|
const emit = defineEmits(["viewSupplier"]);
|
||||||
|
|
||||||
@ -546,10 +750,13 @@ const validationErrors = ref({});
|
|||||||
const productData = ref(null);
|
const productData = ref(null);
|
||||||
const isEditMode = ref(false);
|
const isEditMode = ref(false);
|
||||||
const originalData = ref(null);
|
const originalData = ref(null);
|
||||||
|
const categories = ref([]);
|
||||||
|
const activeTab = ref("details");
|
||||||
|
const stockMovements = ref([]);
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
nom: "",
|
nom: "",
|
||||||
reference: "",
|
reference: "",
|
||||||
categorie: "",
|
categorie_id: "",
|
||||||
fabricant: "",
|
fabricant: "",
|
||||||
numero_lot: "",
|
numero_lot: "",
|
||||||
date_expiration: "",
|
date_expiration: "",
|
||||||
@ -581,6 +788,8 @@ const loadProduct = async () => {
|
|||||||
const response = await productStore.fetchProduct(productId.value);
|
const response = await productStore.fetchProduct(productId.value);
|
||||||
console.log("Product data loaded:", response);
|
console.log("Product data loaded:", response);
|
||||||
productData.value = response;
|
productData.value = response;
|
||||||
|
stockMovements.value =
|
||||||
|
response.stock_moves || response.stockMovements || [];
|
||||||
|
|
||||||
// Initialize form data with current product data
|
// Initialize form data with current product data
|
||||||
initializeFormData(response);
|
initializeFormData(response);
|
||||||
@ -599,7 +808,7 @@ const initializeFormData = (data) => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
nom: data.nom || "",
|
nom: data.nom || "",
|
||||||
reference: data.reference || "",
|
reference: data.reference || "",
|
||||||
categorie: data.categorie || "",
|
categorie_id: (data.categorie_id || data.category?.id)?.toString() || "",
|
||||||
fabricant: data.fabricant || "",
|
fabricant: data.fabricant || "",
|
||||||
numero_lot: data.numero_lot || "",
|
numero_lot: data.numero_lot || "",
|
||||||
date_expiration: data.date_expiration || "",
|
date_expiration: data.date_expiration || "",
|
||||||
@ -615,6 +824,16 @@ const initializeFormData = (data) => {
|
|||||||
validationErrors.value = {};
|
validationErrors.value = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await productCategoryStore.fetchAllCategories();
|
||||||
|
categories.value = response?.data || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading product categories:", err);
|
||||||
|
categories.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.push("/stock/produits");
|
router.push("/stock/produits");
|
||||||
};
|
};
|
||||||
@ -642,7 +861,10 @@ const saveProduct = async () => {
|
|||||||
const productDataToSave = {
|
const productDataToSave = {
|
||||||
nom: formData.value.nom,
|
nom: formData.value.nom,
|
||||||
reference: formData.value.reference,
|
reference: formData.value.reference,
|
||||||
categorie: formData.value.categorie,
|
categorie_id:
|
||||||
|
parseInt(formData.value.categorie_id) ||
|
||||||
|
productData.value?.categorie_id ||
|
||||||
|
null,
|
||||||
fabricant: formData.value.fabricant,
|
fabricant: formData.value.fabricant,
|
||||||
numero_lot: formData.value.numero_lot,
|
numero_lot: formData.value.numero_lot,
|
||||||
date_expiration: formData.value.date_expiration || null,
|
date_expiration: formData.value.date_expiration || null,
|
||||||
@ -710,7 +932,7 @@ const getFieldLabel = (field) => {
|
|||||||
const labels = {
|
const labels = {
|
||||||
nom: "Nom du produit",
|
nom: "Nom du produit",
|
||||||
reference: "Référence",
|
reference: "Référence",
|
||||||
categorie: "Catégorie",
|
categorie_id: "Catégorie",
|
||||||
fabricant: "Fabricant",
|
fabricant: "Fabricant",
|
||||||
numero_lot: "Numéro de lot",
|
numero_lot: "Numéro de lot",
|
||||||
date_expiration: "Date d'expiration",
|
date_expiration: "Date d'expiration",
|
||||||
@ -745,6 +967,23 @@ const hasConditioning = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const displayCategoryName = computed(() => {
|
||||||
|
if (productData.value?.category?.name) {
|
||||||
|
return productData.value.category.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentCategoryId = productData.value?.categorie_id;
|
||||||
|
if (!currentCategoryId) {
|
||||||
|
return "Non catégorisé";
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedCategory = categories.value.find(
|
||||||
|
(category) => category.id === Number(currentCategoryId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return matchedCategory?.name || "Non catégorisé";
|
||||||
|
});
|
||||||
|
|
||||||
const formattedExpiration = computed(() => {
|
const formattedExpiration = computed(() => {
|
||||||
if (!productData.value?.date_expiration) return "Non renseignée";
|
if (!productData.value?.date_expiration) return "Non renseignée";
|
||||||
return new Date(productData.value.date_expiration).toLocaleDateString(
|
return new Date(productData.value.date_expiration).toLocaleDateString(
|
||||||
@ -765,6 +1004,7 @@ const expirationStatusClass = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadCategories();
|
||||||
loadProduct();
|
loadProduct();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -1021,6 +1261,70 @@ onMounted(() => {
|
|||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-side-nav-card {
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar-profile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar-image {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar-reference {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sidebar-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-nav .nav-link {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #67748e;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-nav .nav-link:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-nav .nav-link.active {
|
||||||
|
background: linear-gradient(310deg, #7928ca, #ff0080);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.09),
|
||||||
|
0 2px 3px -1px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-nav .nav-link i {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.document-link {
|
.document-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -1,67 +1,131 @@
|
|||||||
<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>
|
||||||
<div class="mt-sm-0 mt-3">
|
</div>
|
||||||
<soft-button color="secondary" variant="gradient" class="me-2" @click="handleBack">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i> Retour
|
<div v-else-if="error" class="text-center py-5 text-danger">
|
||||||
|
{{ error }}
|
||||||
|
</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>
|
||||||
<soft-button color="info" variant="gradient" class="me-2" @click="handleEdit">
|
|
||||||
<i class="fas fa-edit me-2"></i> Modifier
|
<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>
|
||||||
<soft-button color="danger" variant="gradient" @click="handleDelete">
|
|
||||||
<i class="fas fa-trash me-2"></i> Supprimer
|
<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>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<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">
|
<div class="col-md-4">
|
||||||
<div class="card">
|
<label class="form-label">Numéro réception</label>
|
||||||
<div class="card-header pb-0">
|
<div class="info-value">{{ goodsReceipt.receipt_number }}</div>
|
||||||
<h6>Informations Générales</h6>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<label class="form-label">Date réception</label>
|
||||||
<label class="text-xs font-weight-bold">Numéro de Réception</label>
|
<div class="info-value">{{ formatDate(goodsReceipt.receipt_date) }}</div>
|
||||||
<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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="row g-3">
|
||||||
<div class="card">
|
<div class="col-md-6">
|
||||||
<div class="card-header pb-0">
|
<label class="form-label">Commande fournisseur</label>
|
||||||
<h6>Lignes de Réception</h6>
|
<div class="info-value">
|
||||||
|
{{ goodsReceipt.purchase_order?.po_number || goodsReceipt.purchase_order_id }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</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">
|
<div class="table-responsive">
|
||||||
<table class="table table-flush">
|
<table class="table table-flush">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
@ -75,36 +139,22 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="line in goodsReceipt.lines" :key="line.id">
|
<tr v-for="line in receiptLines" :key="line.id">
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex flex-column">
|
||||||
<div>
|
<span class="text-sm fw-bold">{{ line.product?.nom || `Produit #${line.product_id}` }}</span>
|
||||||
<p class="text-xs font-weight-bold mb-0">
|
<span class="text-xs text-secondary">{{ line.product?.reference || "-" }}</span>
|
||||||
{{ line.product?.nom || 'Produit ' + line.product_id }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-secondary mb-0">
|
|
||||||
{{ line.product?.reference || '' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs">
|
<td class="text-sm">{{ line.packaging?.name || "Unité" }}</td>
|
||||||
{{ line.packaging?.name || 'Unité' }}
|
<td class="text-sm">{{ line.packages_qty_received || "-" }}</td>
|
||||||
</td>
|
<td class="text-sm">{{ line.units_qty_received || "-" }}</td>
|
||||||
<td class="text-xs">
|
<td class="text-sm">{{ line.unit_price ? formatCurrency(line.unit_price) : "-" }}</td>
|
||||||
{{ line.packages_qty_received || '-' }}
|
<td class="text-sm">
|
||||||
</td>
|
{{ line.tva_rate ? `${line.tva_rate.name} (${line.tva_rate.rate}%)` : "-" }}
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!goodsReceipt.lines || goodsReceipt.lines.length === 0">
|
<tr v-if="receiptLines.length === 0">
|
||||||
<td colspan="6" class="text-center text-muted py-4">
|
<td colspan="6" class="text-center text-muted py-4">
|
||||||
Aucune ligne dans cette réception.
|
Aucune ligne dans cette réception.
|
||||||
</td>
|
</td>
|
||||||
@ -115,55 +165,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="container-fluid py-4">
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</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 ?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
|
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
|
||||||
router.push("/stock/receptions");
|
router.push("/stock/receptions");
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error("Failed to delete goods receipt", error);
|
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" }}
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
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>
|
||||||
|
|
||||||
|
<hr class="horizontal dark my-2 mx-3" />
|
||||||
|
|
||||||
|
<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="row">
|
||||||
<div class="col-md-8 d-flex align-items-center">
|
<div class="col-md-6">
|
||||||
<h6 class="mb-0">Détails de l'entrepôt: {{ warehouse.name }}</h6>
|
<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>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-6">
|
||||||
<soft-button color="info" variant="outline" size="sm" @click="handleEdit">
|
<label>Pays</label>
|
||||||
<i class="fas fa-user-edit me-2"></i> Modifier
|
<input
|
||||||
</soft-button>
|
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 class="card-body p-3">
|
</div>
|
||||||
<warehouse-detail-info :warehouse="warehouse" />
|
</div>
|
||||||
<hr class="horizontal dark my-4" />
|
|
||||||
<div class="d-flex justify-content-between">
|
<div v-show="activeTab === 'products'">
|
||||||
<soft-button color="secondary" variant="gradient" @click="handleBack">
|
<div class="card">
|
||||||
<i class="fas fa-arrow-left me-2"></i> Retour à la liste
|
<div class="card-body">
|
||||||
</soft-button>
|
<h5 class="font-weight-bolder mb-4">
|
||||||
<soft-button color="danger" variant="outline" @click="handleDelete">
|
Produits dans l'entrepôt
|
||||||
<i class="fas fa-trash me-2"></i> Supprimer
|
</h5>
|
||||||
</soft-button>
|
|
||||||
|
<div v-if="warehouseProducts.length === 0" class="text-muted">
|
||||||
|
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 ?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleting.value = true;
|
||||||
try {
|
try {
|
||||||
await warehouseStore.deleteWarehouse(props.warehouseId);
|
await warehouseStore.deleteWarehouse(numericWarehouseId.value);
|
||||||
router.push("/stock/warehouses");
|
router.push("/stock/warehouses");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete warehouse", 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;
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
<template #webmailing-header>
|
<template #webmailing-header>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-0">
|
<h3 class="mb-0"><i class="fas fa-envelope"></i> Webmailing</h3>
|
||||||
<i class="fas fa-envelope"></i> Webmailing
|
<small class="text-muted"
|
||||||
</h3>
|
>Gérez vos campagnes d'email marketing</small
|
||||||
<small class="text-muted">Gérez vos campagnes d'email marketing</small>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -17,8 +17,8 @@
|
|||||||
<button
|
<button
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:class="{ active: activeTab === 'compose' }"
|
:class="{ active: activeTab === 'compose' }"
|
||||||
@click="activeTab = 'compose'"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
@click="activeTab = 'compose'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-pen"></i> Composer
|
<i class="fas fa-pen"></i> Composer
|
||||||
</button>
|
</button>
|
||||||
@ -27,8 +27,8 @@
|
|||||||
<button
|
<button
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:class="{ active: activeTab === 'history' }"
|
:class="{ active: activeTab === 'history' }"
|
||||||
@click="activeTab = 'history'"
|
|
||||||
type="button"
|
type="button"
|
||||||
|
@click="activeTab = 'history'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-history"></i> Historique
|
<i class="fas fa-history"></i> Historique
|
||||||
</button>
|
</button>
|
||||||
@ -45,17 +45,10 @@
|
|||||||
@form-data-change="updateFormData"
|
@form-data-change="updateFormData"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||||
<soft-button
|
<soft-button color="secondary" variant="outline" @click="resetForm">
|
||||||
color="secondary"
|
|
||||||
variant="outline"
|
|
||||||
@click="resetForm"
|
|
||||||
>
|
|
||||||
<i class="fas fa-redo"></i> Réinitialiser
|
<i class="fas fa-redo"></i> Réinitialiser
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button
|
<soft-button color="success" @click="sendEmail">
|
||||||
color="success"
|
|
||||||
@click="sendEmail"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane"></i> Envoyer
|
<i class="fas fa-paper-plane"></i> Envoyer
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,21 +8,13 @@
|
|||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
>
|
>
|
||||||
<option value="">-- Sélectionner un type --</option>
|
<option value="">-- Sélectionner un type --</option>
|
||||||
<option value="text">
|
<option value="text"><i class="fas fa-comment"></i> Texte</option>
|
||||||
<i class="fas fa-comment"></i> Texte
|
|
||||||
</option>
|
|
||||||
<option value="phone">
|
<option value="phone">
|
||||||
<i class="fas fa-phone"></i> Appel téléphonique
|
<i class="fas fa-phone"></i> Appel téléphonique
|
||||||
</option>
|
</option>
|
||||||
<option value="email">
|
<option value="email"><i class="fas fa-envelope"></i> Email</option>
|
||||||
<i class="fas fa-envelope"></i> Email
|
<option value="meeting"><i class="fas fa-calendar"></i> Réunion</option>
|
||||||
</option>
|
<option value="note"><i class="fas fa-sticky-note"></i> Note</option>
|
||||||
<option value="meeting">
|
|
||||||
<i class="fas fa-calendar"></i> Réunion
|
|
||||||
</option>
|
|
||||||
<option value="note">
|
|
||||||
<i class="fas fa-sticky-note"></i> Note
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
class="btn d-inline-flex align-items-center justify-content-center gap-2 border-0 shadow-sm transition-all"
|
class="btn d-inline-flex align-items-center justify-content-center gap-2 border-0 shadow-sm transition-all"
|
||||||
:class="[
|
:class="[
|
||||||
variant === 'primary' ? 'btn-primary-gradient' : 'btn-outline-indigo',
|
variant === 'primary' ? 'btn-primary-gradient' : 'btn-outline-indigo',
|
||||||
size === 'sm' ? 'btn-sm py-1 px-3' : 'py-2 px-4'
|
size === 'sm' ? 'btn-sm py-1 px-3' : 'py-2 px-4',
|
||||||
]"
|
]"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
@ -20,16 +20,16 @@ import { defineProps, defineEmits } from "vue";
|
|||||||
defineProps({
|
defineProps({
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "secondary" // primary or secondary
|
default: "secondary", // primary or secondary
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "md"
|
default: "md",
|
||||||
},
|
},
|
||||||
hideTextOnMobile: {
|
hideTextOnMobile: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["click"]);
|
defineEmits(["click"]);
|
||||||
|
|||||||
@ -22,7 +22,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<select v-model="localPeriod" class="form-select" @change="handlePeriodChange">
|
<select
|
||||||
|
v-model="localPeriod"
|
||||||
|
class="form-select"
|
||||||
|
@change="handlePeriodChange"
|
||||||
|
>
|
||||||
<option value="">-- Période personnalisée --</option>
|
<option value="">-- Période personnalisée --</option>
|
||||||
<option value="today">Aujourd'hui</option>
|
<option value="today">Aujourd'hui</option>
|
||||||
<option value="week">Cette semaine</option>
|
<option value="week">Cette semaine</option>
|
||||||
|
|||||||
@ -54,9 +54,7 @@ const trendClass = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const trendIcon = computed(() => {
|
const trendIcon = computed(() => {
|
||||||
return props.trendPositive
|
return props.trendPositive ? "fas fa-arrow-up" : "fas fa-arrow-down";
|
||||||
? "fas fa-arrow-up"
|
|
||||||
: "fas fa-arrow-down";
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,9 @@
|
|||||||
multiple
|
multiple
|
||||||
@change="handleFileChange"
|
@change="handleFileChange"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted">Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small>
|
<small class="text-muted"
|
||||||
|
>Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 card-body">
|
<div class="p-3 card-body">
|
||||||
<div ref="calendarEl" :id="calendarId" data-toggle="widget-calendar"></div>
|
<div
|
||||||
|
:id="calendarId"
|
||||||
|
ref="calendarEl"
|
||||||
|
data-toggle="widget-calendar"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -4,9 +4,7 @@
|
|||||||
<h3 class="mb-1">
|
<h3 class="mb-1">
|
||||||
<strong>{{ avoirNumber }}</strong>
|
<strong>{{ avoirNumber }}</strong>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">Créé le {{ formatDate(date) }}</p>
|
||||||
Créé le {{ formatDate(date) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="submitForm" class="space-y-6">
|
<form class="space-y-6" @submit.prevent="submitForm">
|
||||||
<!-- Row 1: N° Avoir, Date d'émission, Statut -->
|
<!-- Row 1: N° Avoir, Date d'émission, Statut -->
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -13,10 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Date d'émission</label>
|
<label class="form-label">Date d'émission</label>
|
||||||
<soft-input
|
<soft-input v-model="formData.date" type="date" />
|
||||||
v-model="formData.date"
|
|
||||||
type="date"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Statut</label>
|
<label class="form-label">Statut</label>
|
||||||
@ -39,23 +36,32 @@
|
|||||||
v-model="invoiceSearchQuery"
|
v-model="invoiceSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher une facture..."
|
placeholder="Rechercher une facture..."
|
||||||
|
class="search-input"
|
||||||
@input="handleInvoiceSearch"
|
@input="handleInvoiceSearch"
|
||||||
@focus="showInvoiceResults = true"
|
@focus="showInvoiceResults = true"
|
||||||
class="search-input"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Results Dropdown -->
|
<!-- Search Results Dropdown -->
|
||||||
<div
|
<div
|
||||||
v-if="showInvoiceResults && (invoiceSearchResults.length > 0 || isSearchingInvoices)"
|
v-if="
|
||||||
|
showInvoiceResults &&
|
||||||
|
(invoiceSearchResults.length > 0 || isSearchingInvoices)
|
||||||
|
"
|
||||||
class="search-dropdown"
|
class="search-dropdown"
|
||||||
>
|
>
|
||||||
<div v-if="isSearchingInvoices" class="dropdown-loading">
|
<div v-if="isSearchingInvoices" 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="invoiceSearchResults.length === 0" class="dropdown-empty">
|
<div
|
||||||
|
v-else-if="invoiceSearchResults.length === 0"
|
||||||
|
class="dropdown-empty"
|
||||||
|
>
|
||||||
Aucune facture trouvée
|
Aucune facture trouvée
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -94,14 +100,18 @@
|
|||||||
<option value="erreur_facturation">Erreur de facturation</option>
|
<option value="erreur_facturation">Erreur de facturation</option>
|
||||||
<option value="retour_marchandise">Retour de marchandise</option>
|
<option value="retour_marchandise">Retour de marchandise</option>
|
||||||
<option value="geste_commercial">Geste commercial</option>
|
<option value="geste_commercial">Geste commercial</option>
|
||||||
<option value="annulation_prestation">Annulation de prestation</option>
|
<option value="annulation_prestation">
|
||||||
|
Annulation de prestation
|
||||||
|
</option>
|
||||||
<option value="autre">Autre</option>
|
<option value="autre">Autre</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Mode de remboursement</label>
|
<label class="form-label">Mode de remboursement</label>
|
||||||
<select v-model="formData.refundMethod" class="form-select">
|
<select v-model="formData.refundMethod" class="form-select">
|
||||||
<option value="deduction_facture">Déduction sur prochaine facture</option>
|
<option value="deduction_facture">
|
||||||
|
Déduction sur prochaine facture
|
||||||
|
</option>
|
||||||
<option value="virement">Virement bancaire</option>
|
<option value="virement">Virement bancaire</option>
|
||||||
<option value="cheque">Chèque</option>
|
<option value="cheque">Chèque</option>
|
||||||
<option value="especes">Espèces</option>
|
<option value="especes">Espèces</option>
|
||||||
@ -125,12 +135,7 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<label class="form-label mb-0">Lignes de l'avoir</label>
|
<label class="form-label mb-0">Lignes de l'avoir</label>
|
||||||
<soft-button
|
<soft-button type="button" color="primary" size="sm" @click="addLine">
|
||||||
type="button"
|
|
||||||
color="primary"
|
|
||||||
size="sm"
|
|
||||||
@click="addLine"
|
|
||||||
>
|
|
||||||
<i class="fas fa-plus me-1"></i> Ajouter une ligne
|
<i class="fas fa-plus me-1"></i> Ajouter une ligne
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
@ -171,8 +176,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-danger"
|
class="btn btn-sm btn-danger"
|
||||||
@click="removeLine(index)"
|
|
||||||
:disabled="formData.lines.length === 1"
|
:disabled="formData.lines.length === 1"
|
||||||
|
@click="removeLine(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -196,7 +201,9 @@
|
|||||||
<span>{{ formatCurrency(calculateTotalTva()) }}</span>
|
<span>{{ formatCurrency(calculateTotalTva()) }}</span>
|
||||||
</p>
|
</p>
|
||||||
<h5 class="mb-0 text-info">
|
<h5 class="mb-0 text-info">
|
||||||
<strong>Total TTC : {{ formatCurrency(calculateTotalTtc()) }}</strong>
|
<strong
|
||||||
|
>Total TTC : {{ formatCurrency(calculateTotalTtc()) }}</strong
|
||||||
|
>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -225,12 +232,7 @@
|
|||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button
|
<soft-button type="submit" color="success"> Créer l'avoir </soft-button>
|
||||||
type="submit"
|
|
||||||
color="success"
|
|
||||||
>
|
|
||||||
Créer l'avoir
|
|
||||||
</soft-button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@ -312,9 +314,10 @@ const handleInvoiceSearch = () => {
|
|||||||
showInvoiceResults.value = true;
|
showInvoiceResults.value = true;
|
||||||
try {
|
try {
|
||||||
// Simulate API search
|
// Simulate API search
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
const query = invoiceSearchQuery.value.toLowerCase();
|
const query = invoiceSearchQuery.value.toLowerCase();
|
||||||
invoiceSearchResults.value = sampleInvoices.filter(invoice =>
|
invoiceSearchResults.value = sampleInvoices.filter(
|
||||||
|
(invoice) =>
|
||||||
invoice.invoice_number.toLowerCase().includes(query) ||
|
invoice.invoice_number.toLowerCase().includes(query) ||
|
||||||
invoice.clientName.toLowerCase().includes(query)
|
invoice.clientName.toLowerCase().includes(query)
|
||||||
);
|
);
|
||||||
@ -349,18 +352,18 @@ const selectInvoice = (invoice) => {
|
|||||||
|
|
||||||
// Close dropdowns on click outside
|
// Close dropdowns on click outside
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const invoiceContainer = document.querySelector('.invoice-search-container');
|
const invoiceContainer = document.querySelector(".invoice-search-container");
|
||||||
if (invoiceContainer && !invoiceContainer.contains(event.target)) {
|
if (invoiceContainer && !invoiceContainer.contains(event.target)) {
|
||||||
showInvoiceResults.value = false;
|
showInvoiceResults.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({
|
||||||
@ -420,7 +423,10 @@ const formatCurrency = (value) => {
|
|||||||
|
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
if (!formData.value.invoiceId) {
|
if (!formData.value.invoiceId) {
|
||||||
notificationStore.error("Erreur", "Veuillez sélectionner une facture d'origine");
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Veuillez sélectionner une facture d'origine"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formData.value.lines.length === 0) {
|
if (formData.value.lines.length === 0) {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-4">{{ isEdit ? "Modifier le groupe" : "Nouveau groupe" }}</h5>
|
<h5 class="mb-4">
|
||||||
|
{{ isEdit ? "Modifier le groupe" : "Nouveau groupe" }}
|
||||||
|
</h5>
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@ -39,8 +41,19 @@
|
|||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button type="submit" color="success" variant="gradient" :disabled="loading">
|
<soft-button
|
||||||
{{ loading ? "Enregistrement..." : isEdit ? "Mettre à jour" : "Créer" }}
|
type="submit"
|
||||||
|
color="success"
|
||||||
|
variant="gradient"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
loading
|
||||||
|
? "Enregistrement..."
|
||||||
|
: isEdit
|
||||||
|
? "Mettre à jour"
|
||||||
|
: "Créer"
|
||||||
|
}}
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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,5 +1,5 @@
|
|||||||
<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">
|
||||||
@ -10,27 +10,35 @@
|
|||||||
<!-- 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,7 +52,8 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
@ -52,12 +61,10 @@
|
|||||||
</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,17 +135,22 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
@ -148,11 +160,17 @@
|
|||||||
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,7 +183,9 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
@ -173,7 +193,9 @@
|
|||||||
</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"
|
||||||
@ -183,7 +205,9 @@
|
|||||||
</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"
|
||||||
@ -194,7 +218,9 @@
|
|||||||
</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"
|
||||||
@ -208,9 +234,9 @@
|
|||||||
<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 {
|
||||||
@ -324,9 +354,11 @@ const selectSupplier = (supplier) => {
|
|||||||
|
|
||||||
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 (
|
||||||
|
activeLineIndex.value === index &&
|
||||||
formData.value.lines[index] &&
|
formData.value.lines[index] &&
|
||||||
formData.value.lines[index].searchQuery === query) {
|
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 = [];
|
||||||
@ -377,7 +409,7 @@ const handleProductSearch = (index) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -408,14 +440,18 @@ const selectProduct = (index, product) => {
|
|||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
@ -427,11 +463,11 @@ const handleClickOutside = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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,14 +547,14 @@ 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);
|
||||||
@ -524,10 +563,11 @@ const submitForm = async () => {
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,16 +3,24 @@
|
|||||||
<table class="table align-items-center mb-0">
|
<table class="table align-items-center mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
Description
|
Description
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Qté
|
Qté
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Prix Unit. HT
|
Prix Unit. HT
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Total HT
|
Total HT
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -27,13 +35,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle text-center text-sm">
|
<td class="align-middle text-center text-sm">
|
||||||
<span class="text-secondary text-xs font-weight-bold">{{ line.quantity }}</span>
|
<span class="text-secondary text-xs font-weight-bold">{{
|
||||||
|
line.quantity
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle text-center text-sm">
|
<td class="align-middle text-center text-sm">
|
||||||
<span class="text-secondary text-xs font-weight-bold">{{ formatCurrency(line.priceHt) }}</span>
|
<span class="text-secondary text-xs font-weight-bold">{{
|
||||||
|
formatCurrency(line.priceHt)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle text-center text-sm">
|
<td class="align-middle text-center text-sm">
|
||||||
<span class="text-secondary text-xs font-weight-bold">{{ formatCurrency(line.totalHt) }}</span>
|
<span class="text-secondary text-xs font-weight-bold">{{
|
||||||
|
formatCurrency(line.totalHt)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-sm-flex justify-content-between">
|
<div class="d-sm-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<soft-button color="success" variant="gradient" size="sm" @click="$emit('create')">
|
<soft-button
|
||||||
|
color="success"
|
||||||
|
variant="gradient"
|
||||||
|
size="sm"
|
||||||
|
@click="$emit('create')"
|
||||||
|
>
|
||||||
<i class="fas fa-plus me-1"></i> Ajouter une facture
|
<i class="fas fa-plus me-1"></i> Ajouter une facture
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,9 @@
|
|||||||
<span class="text-sm">Total HT:</span>
|
<span class="text-sm">Total HT:</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<span class="text-sm text-dark font-weight-bold">{{ formatCurrency(ht) }}</span>
|
<span class="text-sm text-dark font-weight-bold">{{
|
||||||
|
formatCurrency(ht)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -17,15 +19,21 @@
|
|||||||
<span class="text-sm">TVA (20%):</span>
|
<span class="text-sm">TVA (20%):</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<span class="text-sm text-dark font-weight-bold">{{ formatCurrency(tva) }}</span>
|
<span class="text-sm text-dark font-weight-bold">{{
|
||||||
|
formatCurrency(tva)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-sm font-weight-bold text-info">Total TTC:</span>
|
<span class="text-sm font-weight-bold text-info"
|
||||||
|
>Total TTC:</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<span class="text-sm font-weight-bold text-info">{{ formatCurrency(ttc) }}</span>
|
<span class="text-sm font-weight-bold text-info">{{
|
||||||
|
formatCurrency(ttc)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -13,20 +13,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Fournisseur *</label>
|
<label class="form-label">Fournisseur *</label>
|
||||||
<select v-model="formData.supplierId" class="form-select" @change="updateSupplierInfo" required>
|
<select
|
||||||
|
v-model="formData.supplierId"
|
||||||
|
class="form-select"
|
||||||
|
required
|
||||||
|
@change="updateSupplierInfo"
|
||||||
|
>
|
||||||
<option value="">-- Sélectionner un fournisseur --</option>
|
<option value="">-- Sélectionner un fournisseur --</option>
|
||||||
<option v-for="supplier in suppliers" :key="supplier.id" :value="supplier.id">
|
<option
|
||||||
|
v-for="supplier in suppliers"
|
||||||
|
:key="supplier.id"
|
||||||
|
:value="supplier.id"
|
||||||
|
>
|
||||||
{{ supplier.name }}
|
{{ supplier.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Date facture *</label>
|
<label class="form-label">Date facture *</label>
|
||||||
<soft-input
|
<soft-input v-model="formData.date" type="date" required />
|
||||||
v-model="formData.date"
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -55,13 +60,27 @@
|
|||||||
<!-- Articles Section -->
|
<!-- Articles Section -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-lg font-bold">Lignes de facture</label>
|
<label
|
||||||
|
class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-lg font-bold"
|
||||||
|
>Lignes de facture</label
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
|
class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
|
||||||
type="button"
|
type="button"
|
||||||
@click="addLine"
|
@click="addLine"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus w-4 h-4 mr-2">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-plus w-4 h-4 mr-2"
|
||||||
|
>
|
||||||
<path d="M5 12h14"></path>
|
<path d="M5 12h14"></path>
|
||||||
<path d="M12 5v14"></path>
|
<path d="M12 5v14"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@ -114,8 +133,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-link text-danger mb-0"
|
class="btn btn-sm btn-link text-danger mb-0"
|
||||||
@click="removeLine(index)"
|
|
||||||
:disabled="formData.lines.length === 1"
|
:disabled="formData.lines.length === 1"
|
||||||
|
@click="removeLine(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -131,16 +150,22 @@
|
|||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-sm">Total HT:</span>
|
<span class="text-sm">Total HT:</span>
|
||||||
<span class="text-sm font-weight-bold">{{ formatCurrency(calculateTotalHt()) }}</span>
|
<span class="text-sm font-weight-bold">{{
|
||||||
|
formatCurrency(calculateTotalHt())
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-sm">TVA (20%):</span>
|
<span class="text-sm">TVA (20%):</span>
|
||||||
<span class="text-sm font-weight-bold">{{ formatCurrency(calculateTotalTva()) }}</span>
|
<span class="text-sm font-weight-bold">{{
|
||||||
|
formatCurrency(calculateTotalTva())
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<hr class="horizontal dark my-2">
|
<hr class="horizontal dark my-2" />
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<span class="text-base font-weight-bold">Total TTC:</span>
|
<span class="text-base font-weight-bold">Total TTC:</span>
|
||||||
<span class="text-base font-weight-bold text-info">{{ formatCurrency(calculateTotalTtc()) }}</span>
|
<span class="text-base font-weight-bold text-info">{{
|
||||||
|
formatCurrency(calculateTotalTtc())
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -157,10 +182,7 @@
|
|||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<soft-button
|
<soft-button type="submit" color="success">
|
||||||
type="submit"
|
|
||||||
color="success"
|
|
||||||
>
|
|
||||||
Enregistrer la facture
|
Enregistrer la facture
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
@ -201,7 +223,7 @@ const formData = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateSupplierInfo = () => {
|
const updateSupplierInfo = () => {
|
||||||
const supplier = suppliers.find(s => s.id === formData.value.supplierId);
|
const supplier = suppliers.find((s) => s.id === formData.value.supplierId);
|
||||||
if (supplier) {
|
if (supplier) {
|
||||||
formData.value.supplierName = supplier.name;
|
formData.value.supplierName = supplier.name;
|
||||||
}
|
}
|
||||||
@ -254,10 +276,10 @@ const submitForm = () => {
|
|||||||
totalTva: calculateTotalTva(),
|
totalTva: calculateTotalTva(),
|
||||||
totalTtc: calculateTotalTtc(),
|
totalTtc: calculateTotalTtc(),
|
||||||
// Map lines to include totalHt for store consistency
|
// Map lines to include totalHt for store consistency
|
||||||
lines: formData.value.lines.map(line => ({
|
lines: formData.value.lines.map((line) => ({
|
||||||
...line,
|
...line,
|
||||||
totalHt: line.quantity * line.priceHt
|
totalHt: line.quantity * line.priceHt,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
emit("submit", payload);
|
emit("submit", payload);
|
||||||
@ -277,21 +299,54 @@ const submitForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Tailwind-like utilities used in the provided snippet */
|
/* Tailwind-like utilities used in the provided snippet */
|
||||||
.flex { display: flex; }
|
.flex {
|
||||||
.items-center { align-items: center; }
|
display: flex;
|
||||||
.justify-between { justify-content: space-between; }
|
}
|
||||||
.mb-4 { margin-bottom: 1.5rem; }
|
.items-center {
|
||||||
.text-lg { font-size: 1.125rem; }
|
align-items: center;
|
||||||
.font-bold { font-weight: 700; }
|
}
|
||||||
.gap-2 { gap: 0.5rem; }
|
.justify-between {
|
||||||
.whitespace-nowrap { white-space: nowrap; }
|
justify-content: space-between;
|
||||||
.transition-colors { transition-property: background-color, border-color, color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
}
|
||||||
.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
|
.mb-4 {
|
||||||
.rounded-md { border-radius: 0.375rem; }
|
margin-bottom: 1.5rem;
|
||||||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
}
|
||||||
.h-8 { height: 2rem; }
|
.text-lg {
|
||||||
.text-xs { font-size: 0.75rem; }
|
font-size: 1.125rem;
|
||||||
.font-medium { font-weight: 500; }
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.whitespace-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: background-color, border-color, color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
.shadow {
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
.h-8 {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.space-y-3 > div + div {
|
.space-y-3 > div + div {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|||||||
@ -24,15 +24,9 @@
|
|||||||
class="form-select"
|
class="form-select"
|
||||||
@change="emitFormData"
|
@change="emitFormData"
|
||||||
>
|
>
|
||||||
<option value="low">
|
<option value="low"><i class="fas fa-arrow-down"></i> Basse</option>
|
||||||
<i class="fas fa-arrow-down"></i> Basse
|
<option value="normal"><i class="fas fa-minus"></i> Normale</option>
|
||||||
</option>
|
<option value="high"><i class="fas fa-arrow-up"></i> Haute</option>
|
||||||
<option value="normal">
|
|
||||||
<i class="fas fa-minus"></i> Normale
|
|
||||||
</option>
|
|
||||||
<option value="high">
|
|
||||||
<i class="fas fa-arrow-up"></i> Haute
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -69,10 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section mb-4">
|
<div class="form-section mb-4">
|
||||||
<message-content
|
<message-content v-model="formData.content" @blur="emitFormData" />
|
||||||
v-model="formData.content"
|
|
||||||
@blur="emitFormData"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
<i class="fas fa-info-circle"></i> Aucun message
|
<i class="fas fa-info-circle"></i> Aucun message
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-for="message in messages" :key="message.id" class="message-item mb-3">
|
<div
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
class="message-item mb-3"
|
||||||
|
>
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div>
|
||||||
@ -14,19 +18,15 @@
|
|||||||
{{ getTypeLabel(message.type) }}
|
{{ getTypeLabel(message.type) }}
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
</h6>
|
||||||
<small class="text-muted">{{ formatDate(message.createdDate) }}</small>
|
<small class="text-muted">{{
|
||||||
|
formatDate(message.createdDate)
|
||||||
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<span
|
<span v-if="message.priority === 'high'" class="badge bg-danger">
|
||||||
v-if="message.priority === 'high'"
|
|
||||||
class="badge bg-danger"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-circle"></i> Haute priorité
|
<i class="fas fa-exclamation-circle"></i> Haute priorité
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-if="message.isUrgent" class="badge bg-warning">
|
||||||
v-if="message.isUrgent"
|
|
||||||
class="badge bg-warning"
|
|
||||||
>
|
|
||||||
<i class="fas fa-fire"></i> Urgent
|
<i class="fas fa-fire"></i> Urgent
|
||||||
</span>
|
</span>
|
||||||
<span :class="getStatusClass(message.read)">
|
<span :class="getStatusClass(message.read)">
|
||||||
@ -57,13 +57,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-actions mt-2">
|
<div class="message-actions mt-2">
|
||||||
<button class="btn btn-sm btn-outline-primary" @click="markAsRead(message.id)">
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
@click="markAsRead(message.id)"
|
||||||
|
>
|
||||||
<i class="fas fa-envelope-open"></i> Marquer comme lu
|
<i class="fas fa-envelope-open"></i> Marquer comme lu
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-info ms-2" @click="replyMessage(message.id)">
|
<button
|
||||||
|
class="btn btn-sm btn-outline-info ms-2"
|
||||||
|
@click="replyMessage(message.id)"
|
||||||
|
>
|
||||||
<i class="fas fa-reply"></i> Répondre
|
<i class="fas fa-reply"></i> Répondre
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger ms-2" @click="deleteMessage(message.id)">
|
<button
|
||||||
|
class="btn btn-sm btn-outline-danger ms-2"
|
||||||
|
@click="deleteMessage(message.id)"
|
||||||
|
>
|
||||||
<i class="fas fa-trash"></i> Supprimer
|
<i class="fas fa-trash"></i> Supprimer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,22 +3,34 @@
|
|||||||
<table class="table align-items-center mb-0">
|
<table class="table align-items-center mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Produit
|
Produit
|
||||||
</th>
|
</th>
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
Description
|
Description
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Quantité
|
Quantité
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Prix Unit.
|
Prix Unit.
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Remise
|
Remise
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
<th
|
||||||
|
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
Total HT
|
Total HT
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -26,22 +38,34 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(line, index) in lines" :key="index">
|
<tr v-for="(line, index) in lines" :key="index">
|
||||||
<td>
|
<td>
|
||||||
<span class="text-xs font-weight-bold">{{ line.product_name || 'Produit' }}</span>
|
<span class="text-xs font-weight-bold">{{
|
||||||
|
line.product_name || "Produit"
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-xs text-secondary">{{ line.description || '-' }}</span>
|
<span class="text-xs text-secondary">{{
|
||||||
|
line.description || "-"
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span class="text-xs font-weight-bold">{{ line.units_qty || line.qty_base || 1 }}</span>
|
<span class="text-xs font-weight-bold">{{
|
||||||
|
line.units_qty || line.qty_base || 1
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span class="text-xs font-weight-bold">{{ formatCurrency(line.unit_price) }}</span>
|
<span class="text-xs font-weight-bold">{{
|
||||||
|
formatCurrency(line.unit_price)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span class="text-xs font-weight-bold">{{ line.discount_pct || 0 }}%</span>
|
<span class="text-xs font-weight-bold"
|
||||||
|
>{{ line.discount_pct || 0 }}%</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span class="text-xs font-weight-bold">{{ formatCurrency(line.total_ht) }}</span>
|
<span class="text-xs font-weight-bold">{{
|
||||||
|
formatCurrency(line.total_ht)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!lines || lines.length === 0">
|
<tr v-if="!lines || lines.length === 0">
|
||||||
|
|||||||
@ -12,7 +12,9 @@
|
|||||||
<hr class="horizontal dark my-2" />
|
<hr class="horizontal dark my-2" />
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<span class="text-sm font-weight-bold">Total TTC:</span>
|
<span class="text-sm font-weight-bold">Total TTC:</span>
|
||||||
<span class="text-sm font-weight-bold text-success">{{ formatCurrency(ttc) }}</span>
|
<span class="text-sm font-weight-bold text-success">{{
|
||||||
|
formatCurrency(ttc)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,17 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card border-0 shadow-lg rounded-xl h-100 sidebar-card" :class="{ 'collapsed': isCollapsed }">
|
<div
|
||||||
|
class="card border-0 shadow-lg rounded-xl h-100 sidebar-card"
|
||||||
|
:class="{ collapsed: isCollapsed }"
|
||||||
|
>
|
||||||
<div class="card-header bg-white border-0 pb-0">
|
<div class="card-header bg-white border-0 pb-0">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<i class="fas fa-users text-indigo-600"></i>
|
<i class="fas fa-users text-indigo-600"></i>
|
||||||
<h3 class="text-xs font-semibold mb-0">👥 Collaborateurs ({{ count }})</h3>
|
<h3 class="text-xs font-semibold mb-0">
|
||||||
|
👥 Collaborateurs ({{ count }})
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body pt-0">
|
<div class="card-body pt-0">
|
||||||
<div v-if="count === 0" class="text-center text-secondary py-4 text-xs">
|
<div v-if="count === 0" class="text-center text-secondary py-4 text-xs">
|
||||||
<p class="mb-2">Aucun collaborateur</p>
|
<p class="mb-2">Aucun collaborateur</p>
|
||||||
<p class="text-muted opacity-60">💡 Allouez une couleur depuis la page Salariés</p>
|
<p class="text-muted opacity-60">
|
||||||
|
💡 Allouez une couleur depuis la page Salariés
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<slot v-else></slot>
|
<slot v-else></slot>
|
||||||
</div>
|
</div>
|
||||||
@ -26,12 +33,12 @@ import { defineProps } from "vue";
|
|||||||
defineProps({
|
defineProps({
|
||||||
count: {
|
count: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0,
|
||||||
},
|
},
|
||||||
isCollapsed: {
|
isCollapsed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<soft-button color="info" variant="gradient" @click="$emit('select-type', 'intervention')">
|
||||||
|
<i class="fas fa-briefcase-medical me-2"></i>
|
||||||
|
Créer une intervention
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button color="warning" variant="gradient" @click="$emit('select-type', 'leave')">
|
||||||
|
<i class="fas fa-umbrella-beach me-2"></i>
|
||||||
|
Demande de congé employé
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<soft-button color="success" variant="gradient" @click="$emit('select-type', 'event')">
|
||||||
|
<i class="fas fa-calendar-plus me-2"></i>
|
||||||
|
Créer un événement
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
|
defineEmits(["select-type"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
@ -1,31 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex align-items-center bg-white shadow-sm rounded-lg p-1 border">
|
<div
|
||||||
<button
|
class="d-flex align-items-center bg-white shadow-sm rounded-lg p-1 border"
|
||||||
class="btn btn-sm btn-icon mb-0 shadow-none border-0 hover:bg-gray-100 rounded-md"
|
>
|
||||||
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="btn-icon-soft"
|
||||||
@click="$emit('prev-week')"
|
@click="$emit('prev-week')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chevron-left text-secondary text-xs"></i>
|
<i class="fas fa-chevron-left text-secondary text-xs"></i>
|
||||||
</button>
|
</soft-button>
|
||||||
<div class="px-3 py-1 text-sm font-semibold text-dark whitespace-nowrap min-w-140 text-center">
|
<div
|
||||||
|
class="px-3 py-1 text-sm font-semibold text-dark whitespace-nowrap min-w-140 text-center"
|
||||||
|
>
|
||||||
{{ dateRangeDisplay }}
|
{{ dateRangeDisplay }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<soft-button
|
||||||
class="btn btn-sm btn-icon mb-0 shadow-none border-0 hover:bg-gray-100 rounded-md"
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="btn-icon-soft"
|
||||||
@click="$emit('next-week')"
|
@click="$emit('next-week')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chevron-right text-secondary text-xs"></i>
|
<i class="fas fa-chevron-right text-secondary text-xs"></i>
|
||||||
</button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps, defineEmits } from "vue";
|
import { computed, defineProps, defineEmits } from "vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
currentDate: {
|
currentDate: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: () => new Date()
|
default: () => new Date(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["prev-week", "next-week"]);
|
defineEmits(["prev-week", "next-week"]);
|
||||||
@ -43,11 +54,15 @@ const dateRangeDisplay = computed(() => {
|
|||||||
const sunday = new Date(monday);
|
const sunday = new Date(monday);
|
||||||
sunday.setDate(monday.getDate() + 6);
|
sunday.setDate(monday.getDate() + 6);
|
||||||
|
|
||||||
const options = { day: 'numeric', month: 'short' };
|
const options = { day: "numeric", month: "short" };
|
||||||
const startStr = monday.toLocaleDateString('fr-FR', options);
|
const startStr = monday.toLocaleDateString("fr-FR", options);
|
||||||
|
|
||||||
// If same month, don't repeat month in start date (optional refinement, sticking to simple for now)
|
// If same month, don't repeat month in start date (optional refinement, sticking to simple for now)
|
||||||
const endStr = sunday.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
|
const endStr = sunday.toLocaleDateString("fr-FR", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
return `${startStr} - ${endStr}`;
|
return `${startStr} - ${endStr}`;
|
||||||
});
|
});
|
||||||
@ -58,10 +73,6 @@ const dateRangeDisplay = computed(() => {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-md {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-xs {
|
.text-xs {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -78,16 +89,14 @@ const dateRangeDisplay = computed(() => {
|
|||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-gray-100:hover {
|
.btn-icon-soft {
|
||||||
background-color: #f3f4f6;
|
width: 30px;
|
||||||
}
|
height: 30px;
|
||||||
|
min-width: 30px;
|
||||||
.btn-icon {
|
padding: 0 !important;
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<form class="d-grid gap-2" @submit.prevent="$emit('submit')">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Titre</label>
|
||||||
|
<soft-input
|
||||||
|
:model-value="form.title"
|
||||||
|
placeholder="Réunion équipe"
|
||||||
|
@update:model-value="updateField('title', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Date et heure</label>
|
||||||
|
<soft-input
|
||||||
|
:model-value="form.date"
|
||||||
|
type="datetime-local"
|
||||||
|
@update:model-value="updateField('date', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Lieu</label>
|
||||||
|
<soft-input
|
||||||
|
:model-value="form.location"
|
||||||
|
placeholder="Salle principale"
|
||||||
|
@update:model-value="updateField('location', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<input
|
||||||
|
:value="form.description"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Détails de l'événement"
|
||||||
|
@input="updateField('description', $event.target.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 justify-content-end pt-2">
|
||||||
|
<soft-button color="secondary" variant="outline" @click="$emit('back')">Retour</soft-button>
|
||||||
|
<soft-button color="success" variant="gradient" type="submit">Enregistrer</soft-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
defineProps({
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:form", "submit", "back"]);
|
||||||
|
|
||||||
|
const updateField = (field, value) => {
|
||||||
|
emit("update:form", { field, value });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="planning-kanban-container mt-3">
|
<div class="planning-kanban-container mt-3">
|
||||||
<div class="py-2 min-vh-100 d-inline-flex" style="overflow-x: auto">
|
<div class="planning-kanban-scroll py-2">
|
||||||
<div id="planningKanban"></div>
|
<div id="planningKanban"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -92,7 +92,7 @@ const initKanban = () => {
|
|||||||
kanbanInstance = new jKanban({
|
kanbanInstance = new jKanban({
|
||||||
element: "#planningKanban",
|
element: "#planningKanban",
|
||||||
gutter: "10px",
|
gutter: "10px",
|
||||||
widthBoard: "300px",
|
widthBoard: "360px",
|
||||||
responsivePercentage: false,
|
responsivePercentage: false,
|
||||||
dragItems: true,
|
dragItems: true,
|
||||||
boards: boards,
|
boards: boards,
|
||||||
@ -153,12 +153,18 @@ watch(() => props.interventions, () => {
|
|||||||
/* Global styles for jKanban overrides */
|
/* Global styles for jKanban overrides */
|
||||||
.kanban-container {
|
.kanban-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: visible !important;
|
||||||
|
overflow-y: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-board {
|
.kanban-board {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
width: 360px !important;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-board-header {
|
.kanban-board-header {
|
||||||
@ -183,9 +189,27 @@ watch(() => props.interventions, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kanban-drag {
|
.kanban-drag {
|
||||||
min-height: 500px;
|
min-height: 100%;
|
||||||
background-color: #f1f5f9; /* slate-100 */
|
background-color: #f1f5f9; /* slate-100 */
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.planning-kanban-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 220px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-kanban-scroll {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 220px);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#planningKanban {
|
||||||
|
width: 100%;
|
||||||
|
min-width: max-content;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<form class="d-grid gap-2" @submit.prevent="$emit('submit')">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Employé</label>
|
||||||
|
<select :value="form.employee" class="form-select" @change="updateField('employee', $event.target.value)">
|
||||||
|
<option value="" disabled>Choisir un employé</option>
|
||||||
|
<option v-for="collab in collaborators" :key="collab.id" :value="collab.name">
|
||||||
|
{{ collab.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Du</label>
|
||||||
|
<soft-input :model-value="form.startDate" type="date" @update:model-value="updateField('startDate', $event)" />
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Au</label>
|
||||||
|
<soft-input :model-value="form.endDate" type="date" @update:model-value="updateField('endDate', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Motif</label>
|
||||||
|
<soft-input
|
||||||
|
:model-value="form.reason"
|
||||||
|
placeholder="Congé annuel"
|
||||||
|
@update:model-value="updateField('reason', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 justify-content-end pt-2">
|
||||||
|
<soft-button color="secondary" variant="outline" @click="$emit('back')">Retour</soft-button>
|
||||||
|
<soft-button color="warning" variant="gradient" type="submit">Enregistrer</soft-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
defineProps({
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:form", "submit", "back"]);
|
||||||
|
|
||||||
|
const updateField = (field, value) => {
|
||||||
|
emit("update:form", { field, value });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
@ -7,9 +7,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body pt-2">
|
<div class="card-body pt-2">
|
||||||
<div class="d-flex flex-wrap gap-3">
|
<div class="d-flex flex-wrap gap-3">
|
||||||
<div v-for="item in legend" :key="item.label" class="d-flex align-items-center gap-2">
|
<div
|
||||||
|
v-for="item in legend"
|
||||||
|
:key="item.label"
|
||||||
|
class="d-flex align-items-center gap-2"
|
||||||
|
>
|
||||||
<span class="text-sm">{{ item.emoji }}</span>
|
<span class="text-sm">{{ item.emoji }}</span>
|
||||||
<div class="legend-bar rounded-pill" :style="{ backgroundColor: item.color }"></div>
|
<div
|
||||||
|
class="legend-bar rounded-pill"
|
||||||
|
:style="{ backgroundColor: item.color }"
|
||||||
|
></div>
|
||||||
<span class="text-xs text-secondary">{{ item.label }}</span>
|
<span class="text-xs text-secondary">{{ item.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -23,7 +30,7 @@ const legend = [
|
|||||||
{ emoji: "🏥", label: "Maladie", color: "#ef4444" },
|
{ emoji: "🏥", label: "Maladie", color: "#ef4444" },
|
||||||
{ emoji: "📚", label: "Formation", color: "#8b5cf6" },
|
{ emoji: "📚", label: "Formation", color: "#8b5cf6" },
|
||||||
{ emoji: "💼", label: "Sans solde", color: "#6b7280" },
|
{ emoji: "💼", label: "Sans solde", color: "#6b7280" },
|
||||||
{ emoji: "🚑", label: "Accident travail", color: "#dc2626" }
|
{ emoji: "🚑", label: "Accident travail", color: "#dc2626" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -5,56 +5,111 @@
|
|||||||
<table class="table align-items-center mb-0">
|
<table class="table align-items-center mb-0">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-4">Date & Heure</th>
|
<th
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Type</th>
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-4"
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Défunt / Client</th>
|
>
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Collaborateur</th>
|
Date & Heure
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Statut</th>
|
</th>
|
||||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Actions</th>
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Défunt / Client
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Collaborateur
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Statut
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="interventions.length === 0">
|
<tr v-if="interventions.length === 0">
|
||||||
<td colspan="6" class="text-center py-5">
|
<td colspan="6" class="text-center py-5">
|
||||||
<span class="text-muted text-sm">Aucune intervention pour cette période</span>
|
<span class="text-muted text-sm"
|
||||||
|
>Aucune intervention pour cette période</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="intervention in interventions" :key="intervention.id" class="hover-row transition-all">
|
<tr
|
||||||
|
v-for="intervention in interventions"
|
||||||
|
:key="intervention.id"
|
||||||
|
class="hover-row transition-all"
|
||||||
|
>
|
||||||
<td class="ps-4">
|
<td class="ps-4">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<h6 class="mb-0 text-sm font-weight-bold">{{ formatDate(intervention.date) }}</h6>
|
<h6 class="mb-0 text-sm font-weight-bold">
|
||||||
<span class="text-xs text-secondary">{{ formatTime(intervention.date) }}</span>
|
{{ formatDate(intervention.date) }}
|
||||||
|
</h6>
|
||||||
|
<span class="text-xs text-secondary">{{
|
||||||
|
formatTime(intervention.date)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="badge badge-sm bg-gradient-light text-dark mb-0 d-flex align-items-center gap-1 border">
|
<span
|
||||||
<span class="width-8-px height-8-px rounded-circle me-1" :style="{ backgroundColor: getTypeColor(intervention.type) }"></span>
|
class="badge badge-sm bg-gradient-light text-dark mb-0 d-flex align-items-center gap-1 border"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="width-8-px height-8-px rounded-circle me-1"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: getTypeColor(intervention.type),
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
{{ intervention.type }}
|
{{ intervention.type }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<h6 class="mb-0 text-sm">{{ intervention.deceased || 'Non spécifié' }}</h6>
|
<h6 class="mb-0 text-sm">
|
||||||
<span class="text-xs text-secondary">Client: {{ intervention.client || '-' }}</span>
|
{{ intervention.deceased || "Non spécifié" }}
|
||||||
|
</h6>
|
||||||
|
<span class="text-xs text-secondary"
|
||||||
|
>Client: {{ intervention.client || "-" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-primary rounded-circle text-white d-flex align-items-center justify-content-center text-xxs">
|
<div
|
||||||
|
class="avatar avatar-xs me-2 bg-gradient-primary rounded-circle text-white d-flex align-items-center justify-content-center text-xxs"
|
||||||
|
>
|
||||||
{{ getInitials(intervention.collaborator) }}
|
{{ getInitials(intervention.collaborator) }}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm font-weight-bold text-secondary">{{ intervention.collaborator }}</span>
|
<span class="text-sm font-weight-bold text-secondary">{{
|
||||||
|
intervention.collaborator
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-sm" :class="getStatusBadgeClass(intervention.status)">
|
<span
|
||||||
|
class="badge badge-sm"
|
||||||
|
:class="getStatusBadgeClass(intervention.status)"
|
||||||
|
>
|
||||||
{{ intervention.status }}
|
{{ intervention.status }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-link text-secondary mb-0 px-2" @click="$emit('edit', intervention)">
|
<button
|
||||||
|
class="btn btn-link text-secondary mb-0 px-2"
|
||||||
|
@click="$emit('edit', intervention)"
|
||||||
|
>
|
||||||
<i class="fas fa-edit text-xs"></i>
|
<i class="fas fa-edit text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -72,45 +127,57 @@ import { defineProps, defineEmits } from "vue";
|
|||||||
defineProps({
|
defineProps({
|
||||||
interventions: {
|
interventions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["edit"]);
|
defineEmits(["edit"]);
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return "-";
|
if (!dateString) return "-";
|
||||||
return new Date(dateString).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'short' });
|
return new Date(dateString).toLocaleDateString("fr-FR", {
|
||||||
|
weekday: "long",
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (dateString) => {
|
const formatTime = (dateString) => {
|
||||||
if (!dateString) return "-";
|
if (!dateString) return "-";
|
||||||
return new Date(dateString).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
return new Date(dateString).toLocaleTimeString("fr-FR", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitials = (name) => {
|
const getInitials = (name) => {
|
||||||
if (!name) return "?";
|
if (!name) return "?";
|
||||||
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.substring(0, 2)
|
||||||
|
.toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeColor = (type) => {
|
const getTypeColor = (type) => {
|
||||||
const colors = {
|
const colors = {
|
||||||
'Soin': '#3b82f6',
|
Soin: "#3b82f6",
|
||||||
'Transport': '#10b981',
|
Transport: "#10b981",
|
||||||
'Mise en bière': '#f59e0b',
|
"Mise en bière": "#f59e0b",
|
||||||
'Cérémonie': '#8b5cf6'
|
Cérémonie: "#8b5cf6",
|
||||||
};
|
};
|
||||||
return colors[type] || '#6b7280';
|
return colors[type] || "#6b7280";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadgeClass = (status) => {
|
const getStatusBadgeClass = (status) => {
|
||||||
const map = {
|
const map = {
|
||||||
'Confirmé': 'bg-gradient-info',
|
Confirmé: "bg-gradient-info",
|
||||||
'Terminé': 'bg-gradient-success',
|
Terminé: "bg-gradient-success",
|
||||||
'En attente': 'bg-gradient-warning',
|
"En attente": "bg-gradient-warning",
|
||||||
'Annulé': 'bg-gradient-danger'
|
Annulé: "bg-gradient-danger",
|
||||||
};
|
};
|
||||||
return map[status] || 'bg-gradient-secondary';
|
return map[status] || "bg-gradient-secondary";
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button
|
<soft-button
|
||||||
v-for="view in views"
|
v-for="view in views"
|
||||||
:key="view.id"
|
:key="view.id"
|
||||||
class="btn d-inline-flex align-items-center justify-content-center gap-2 border-0 shadow-sm transition-all"
|
class="d-inline-flex align-items-center justify-content-center gap-2 transition-all"
|
||||||
:class="[
|
:color="activeView === view.id ? 'info' : 'secondary'"
|
||||||
activeView === view.id ? 'btn-active' : 'btn-inactive'
|
:variant="activeView === view.id ? 'gradient' : 'outline'"
|
||||||
]"
|
size="sm"
|
||||||
@click="$emit('update:activeView', view.id)"
|
@click="$emit('update:activeView', view.id)"
|
||||||
>
|
>
|
||||||
<i :class="view.icon"></i>
|
<i :class="view.icon"></i>
|
||||||
<span class="d-none d-sm-inline">{{ view.label }}</span>
|
<span class="d-none d-sm-inline">{{ view.label }}</span>
|
||||||
</button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
activeView: {
|
activeView: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "grille"
|
default: "grille",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["update:activeView"]);
|
defineEmits(["update:activeView"]);
|
||||||
@ -30,35 +31,16 @@ defineEmits(["update:activeView"]);
|
|||||||
const views = [
|
const views = [
|
||||||
{ id: "liste", label: "Liste", icon: "fas fa-list", color: "gray" },
|
{ id: "liste", label: "Liste", icon: "fas fa-list", color: "gray" },
|
||||||
{ id: "kanban", label: "Kanban", icon: "fas fa-columns", color: "gray" },
|
{ id: "kanban", label: "Kanban", icon: "fas fa-columns", color: "gray" },
|
||||||
{ id: "grille", label: "Grille", icon: "fas fa-calendar-alt", color: "indigo" }
|
{
|
||||||
|
id: "grille",
|
||||||
|
label: "Grille",
|
||||||
|
icon: "fas fa-calendar-alt",
|
||||||
|
color: "indigo",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.btn {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-active {
|
|
||||||
background-color: #4f46e5;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-inactive {
|
|
||||||
background-color: white;
|
|
||||||
color: #64748b;
|
|
||||||
border: 1px solid #e2e8f0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-inactive:hover {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition-all {
|
.transition-all {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="calendar-grid-container card h-100 border-0 shadow-sm rounded-xl">
|
<div class="calendar-grid-container card h-100 border-0 shadow-sm rounded-xl">
|
||||||
<div class="card-body p-3 h-100">
|
<div class="card-body p-3 h-100">
|
||||||
<div ref="calendarEl" id="fullCalendarGrid" class="h-100"></div>
|
<div id="fullCalendarGrid" ref="calendarEl" class="h-100"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -16,12 +16,12 @@ import frLocale from "@fullcalendar/core/locales/fr";
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
startDate: {
|
startDate: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: () => new Date()
|
default: () => new Date(),
|
||||||
},
|
},
|
||||||
interventions: {
|
interventions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["cell-click", "edit"]);
|
const emit = defineEmits(["cell-click", "edit"]);
|
||||||
@ -49,7 +49,7 @@ const initializeCalendar = () => {
|
|||||||
contentHeight: "auto",
|
contentHeight: "auto",
|
||||||
expandRows: true,
|
expandRows: true,
|
||||||
stickyHeaderDates: true,
|
stickyHeaderDates: true,
|
||||||
dayHeaderFormat: { weekday: 'long', day: 'numeric', month: 'short' }, // "Lundi 12 janv."
|
dayHeaderFormat: { weekday: "long", day: "numeric", month: "short" }, // "Lundi 12 janv."
|
||||||
events: mapEvents(props.interventions),
|
events: mapEvents(props.interventions),
|
||||||
eventClick: (info) => {
|
eventClick: (info) => {
|
||||||
const originalEvent = info.event.extendedProps.originalData;
|
const originalEvent = info.event.extendedProps.originalData;
|
||||||
@ -61,22 +61,22 @@ const initializeCalendar = () => {
|
|||||||
},
|
},
|
||||||
// Styling customization via class names injection if needed
|
// Styling customization via class names injection if needed
|
||||||
eventClassNames: (arg) => {
|
eventClassNames: (arg) => {
|
||||||
return ['shadow-sm', 'border-0'];
|
return ["shadow-sm", "border-0"];
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
calendar.render();
|
calendar.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapEvents = (interventions) => {
|
const mapEvents = (interventions) => {
|
||||||
return interventions.map(i => {
|
return interventions.map((i) => {
|
||||||
// Map props.interventions structure to FullCalendar event object
|
// Map props.interventions structure to FullCalendar event object
|
||||||
// Assuming intervention has: id, date (ISO string), title (or type), status, color
|
// Assuming intervention has: id, date (ISO string), title (or type), status, color
|
||||||
const typeColors = {
|
const typeColors = {
|
||||||
'Soin': '#3b82f6',
|
Soin: "#3b82f6",
|
||||||
'Transport': '#10b981',
|
Transport: "#10b981",
|
||||||
'Mise en bière': '#f59e0b',
|
"Mise en bière": "#f59e0b",
|
||||||
'Cérémonie': '#8b5cf6'
|
Cérémonie: "#8b5cf6",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default duration 1 hour if not specified
|
// Default duration 1 hour if not specified
|
||||||
@ -88,12 +88,12 @@ const mapEvents = (interventions) => {
|
|||||||
title: i.deceased ? `${i.type} - ${i.deceased}` : i.type,
|
title: i.deceased ? `${i.type} - ${i.deceased}` : i.type,
|
||||||
start: i.date,
|
start: i.date,
|
||||||
end: i.end || end, // Use provided end or default
|
end: i.end || end, // Use provided end or default
|
||||||
backgroundColor: typeColors[i.type] || '#6b7280',
|
backgroundColor: typeColors[i.type] || "#6b7280",
|
||||||
borderColor: typeColors[i.type] || '#6b7280',
|
borderColor: typeColors[i.type] || "#6b7280",
|
||||||
textColor: '#ffffff',
|
textColor: "#ffffff",
|
||||||
extendedProps: {
|
extendedProps: {
|
||||||
originalData: i
|
originalData: i,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -102,19 +102,25 @@ onMounted(() => {
|
|||||||
initializeCalendar();
|
initializeCalendar();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.startDate, (newDate) => {
|
watch(
|
||||||
|
() => props.startDate,
|
||||||
|
(newDate) => {
|
||||||
if (calendar) {
|
if (calendar) {
|
||||||
calendar.gotoDate(newDate);
|
calendar.gotoDate(newDate);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(() => props.interventions, (newInterventions) => {
|
watch(
|
||||||
|
() => props.interventions,
|
||||||
|
(newInterventions) => {
|
||||||
if (calendar) {
|
if (calendar) {
|
||||||
calendar.removeAllEvents();
|
calendar.removeAllEvents();
|
||||||
calendar.addEventSource(mapEvents(newInterventions));
|
calendar.addEventSource(mapEvents(newInterventions));
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -147,7 +153,8 @@ watch(() => props.interventions, (newInterventions) => {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.fc td), :deep(.fc th) {
|
:deep(.fc td),
|
||||||
|
:deep(.fc th) {
|
||||||
border-color: #e9ecef;
|
border-color: #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,8 +92,8 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatCurrency = (value) => {
|
const formatCurrency = (value) => {
|
||||||
const numberValue = typeof value === 'string' ? parseFloat(value) : value;
|
const numberValue = typeof value === "string" ? parseFloat(value) : value;
|
||||||
if (isNaN(numberValue)) return '0,00 €';
|
if (isNaN(numberValue)) return "0,00 €";
|
||||||
|
|
||||||
return new Intl.NumberFormat("fr-FR", {
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
|
|||||||
@ -32,8 +32,8 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatCurrency = (value) => {
|
const formatCurrency = (value) => {
|
||||||
const numberValue = typeof value === 'string' ? parseFloat(value) : value;
|
const numberValue = typeof value === "string" ? parseFloat(value) : value;
|
||||||
if (isNaN(numberValue)) return '0,00 €';
|
if (isNaN(numberValue)) return "0,00 €";
|
||||||
|
|
||||||
return new Intl.NumberFormat("fr-FR", {
|
return new Intl.NumberFormat("fr-FR", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
|
|||||||
@ -33,7 +33,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="satisfaction-stars">
|
<div class="satisfaction-stars">
|
||||||
<span v-for="i in 5" :key="i" class="star" :class="getStarClass(i, practitioner.satisfaction)">
|
<span
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="i"
|
||||||
|
class="star"
|
||||||
|
:class="getStarClass(i, practitioner.satisfaction)"
|
||||||
|
>
|
||||||
<i class="fas fa-star"></i>
|
<i class="fas fa-star"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<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">
|
||||||
@ -10,7 +10,9 @@
|
|||||||
<!-- 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"
|
||||||
|
>Commande Fournisseur <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
v-model="formData.purchase_order_id"
|
v-model="formData.purchase_order_id"
|
||||||
class="form-select custom-select"
|
class="form-select custom-select"
|
||||||
@ -18,7 +20,8 @@
|
|||||||
>
|
>
|
||||||
<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">
|
||||||
@ -27,27 +30,35 @@
|
|||||||
</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,17 +150,22 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
@ -160,11 +175,17 @@
|
|||||||
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,7 +198,8 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
@ -187,11 +209,15 @@
|
|||||||
<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>
|
||||||
@ -234,9 +260,9 @@
|
|||||||
<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 {
|
||||||
@ -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 (
|
||||||
|
activeLineIndex.value === index &&
|
||||||
formData.value.lines[index] &&
|
formData.value.lines[index] &&
|
||||||
formData.value.lines[index].searchQuery === query) {
|
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)) {
|
||||||
@ -374,7 +411,7 @@ const handleProductSearch = (index) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -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,
|
id: 1,
|
||||||
name: response.data.conditionnement_nom || 'Conditionnement',
|
name: response.data.conditionnement_nom || "Conditionnement",
|
||||||
qty_base: response.data.conditionnement_quantite || 1
|
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,14 +466,18 @@ 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;
|
||||||
}
|
}
|
||||||
@ -444,11 +489,11 @@ const handleClickOutside = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
|
.map((line) => ({
|
||||||
product_id: parseInt(line.product_id),
|
product_id: parseInt(line.product_id),
|
||||||
packaging_id: line.packaging_id ? parseInt(line.packaging_id) : null,
|
packaging_id: line.packaging_id ? parseInt(line.packaging_id) : null,
|
||||||
packages_qty_received: line.packages_qty_received,
|
packages_qty_received: line.packages_qty_received,
|
||||||
units_qty_received: line.units_qty_received,
|
units_qty_received: line.units_qty_received,
|
||||||
unit_price: line.unit_price,
|
unit_price: line.unit_price,
|
||||||
})).filter(line => line.product_id),
|
}))
|
||||||
|
.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>
|
||||||
|
|||||||
@ -15,18 +15,22 @@
|
|||||||
<option :value="20">20</option>
|
<option :value="20">20</option>
|
||||||
<option :value="50">50</option>
|
<option :value="50">50</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="text-secondary text-xs font-weight-bold">éléments par page</span>
|
<span class="text-secondary text-xs font-weight-bold"
|
||||||
|
>éléments par page</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text text-body"><i class="fas fa-search" aria-hidden="true"></i></span>
|
<span class="input-group-text text-body"
|
||||||
|
><i class="fas fa-search" aria-hidden="true"></i
|
||||||
|
></span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
placeholder="Rechercher..."
|
placeholder="Rechercher..."
|
||||||
@input="onSearch"
|
@input="onSearch"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -228,16 +232,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Footer -->
|
<!-- Pagination Footer -->
|
||||||
<div v-if="!loading && data.length > 0" class="d-flex justify-content-between align-items-center mt-3 px-3">
|
<div
|
||||||
|
v-if="!loading && data.length > 0"
|
||||||
|
class="d-flex justify-content-between align-items-center mt-3 px-3"
|
||||||
|
>
|
||||||
<div class="text-xs text-secondary font-weight-bold">
|
<div class="text-xs text-secondary font-weight-bold">
|
||||||
Affichage de {{ pagination.from }} à {{ pagination.to }} sur {{ pagination.total }} clients
|
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||||
|
{{ pagination.total }} clients
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination pagination-sm pagination-success mb-0">
|
<ul class="pagination pagination-sm pagination-success mb-0">
|
||||||
<li class="page-item" :class="{ disabled: pagination.current_page === 1 }">
|
<li
|
||||||
<a class="page-link" href="#" aria-label="Previous" @click.prevent="changePage(pagination.current_page - 1)">
|
class="page-item"
|
||||||
<span aria-hidden="true"><i class="fa fa-angle-left" aria-hidden="true"></i></span>
|
:class="{ disabled: pagination.current_page === 1 }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="#"
|
||||||
|
aria-label="Previous"
|
||||||
|
@click.prevent="changePage(pagination.current_page - 1)"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true"
|
||||||
|
><i class="fa fa-angle-left" aria-hidden="true"></i
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@ -247,19 +265,32 @@
|
|||||||
class="page-item"
|
class="page-item"
|
||||||
:class="{ active: pagination.current_page === page }"
|
:class="{ active: pagination.current_page === page }"
|
||||||
>
|
>
|
||||||
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
|
<a class="page-link" href="#" @click.prevent="changePage(page)">{{
|
||||||
|
page
|
||||||
|
}}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="page-item" :class="{ disabled: pagination.current_page === pagination.last_page }">
|
<li
|
||||||
<a class="page-link" href="#" aria-label="Next" @click.prevent="changePage(pagination.current_page + 1)">
|
class="page-item"
|
||||||
<span aria-hidden="true"><i class="fa fa-angle-right" aria-hidden="true"></i></span>
|
:class="{
|
||||||
|
disabled: pagination.current_page === pagination.last_page,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="page-link"
|
||||||
|
href="#"
|
||||||
|
aria-label="Next"
|
||||||
|
@click.prevent="changePage(pagination.current_page + 1)"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true"
|
||||||
|
><i class="fa fa-angle-right" aria-hidden="true"></i
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-if="!loading && data.length === 0" class="empty-state">
|
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
@ -282,7 +313,13 @@ import SoftAvatar from "@/components/SoftAvatar.vue";
|
|||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
const emit = defineEmits(["view", "delete", "page-change", "per-page-change", "search-change"]);
|
const emit = defineEmits([
|
||||||
|
"view",
|
||||||
|
"delete",
|
||||||
|
"page-change",
|
||||||
|
"per-page-change",
|
||||||
|
"search-change",
|
||||||
|
]);
|
||||||
|
|
||||||
// Sample avatar images
|
// Sample avatar images
|
||||||
import img1 from "@/assets/img/team-2.jpg";
|
import img1 from "@/assets/img/team-2.jpg";
|
||||||
@ -327,7 +364,11 @@ const displayedPages = computed(() => {
|
|||||||
const delta = 2;
|
const delta = 2;
|
||||||
const range = [];
|
const range = [];
|
||||||
|
|
||||||
for (let i = Math.max(2, current - delta); i <= Math.min(total - 1, current + delta); i++) {
|
for (
|
||||||
|
let i = Math.max(2, current - delta);
|
||||||
|
i <= Math.min(total - 1, current + delta);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
range.push(i);
|
range.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +384,10 @@ const displayedPages = computed(() => {
|
|||||||
range.push(total);
|
range.push(total);
|
||||||
}
|
}
|
||||||
|
|
||||||
return range.filter((val, index, self) => val !== "..." || (val === "..." && self[index - 1] !== "..."));
|
return range.filter(
|
||||||
|
(val, index, self) =>
|
||||||
|
val !== "..." || (val === "..." && self[index - 1] !== "...")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
|||||||
@ -31,7 +31,9 @@
|
|||||||
|
|
||||||
<!-- Created At -->
|
<!-- Created At -->
|
||||||
<td class="text-sm font-weight-bold">
|
<td class="text-sm font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{ formatDate(group.created_at) }}</span>
|
<span class="my-2 text-xs">{{
|
||||||
|
formatDate(group.created_at)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
@ -71,7 +73,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onUnmounted, defineProps, defineEmits } from "vue";
|
import {
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
watch,
|
||||||
|
onUnmounted,
|
||||||
|
defineProps,
|
||||||
|
defineEmits,
|
||||||
|
} from "vue";
|
||||||
import { DataTable } from "simple-datatables";
|
import { DataTable } from "simple-datatables";
|
||||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||||
|
|
||||||
|
|||||||
@ -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 -->
|
||||||
|
|||||||
@ -36,7 +36,9 @@
|
|||||||
|
|
||||||
<!-- Total TTC -->
|
<!-- Total TTC -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{ formatCurrency(facture.totalTtc) }}</span>
|
<span class="my-2 text-xs">{{
|
||||||
|
formatCurrency(facture.totalTtc)
|
||||||
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
@ -47,7 +49,10 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
>
|
>
|
||||||
<i :class="getStatusIcon(facture.status)" aria-hidden="true"></i>
|
<i
|
||||||
|
:class="getStatusIcon(facture.status)"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<span>{{ getStatusLabel(facture.status) }}</span>
|
<span>{{ getStatusLabel(facture.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
<!-- Due Date -->
|
<!-- Due Date -->
|
||||||
<td class="font-weight-bold">
|
<td class="font-weight-bold">
|
||||||
<span class="my-2 text-xs" :class="getDueDateClass(invoice)">{{
|
<span class="my-2 text-xs" :class="getDueDateClass(invoice)">{{
|
||||||
formatDate(invoice.due_date) || '-'
|
formatDate(invoice.due_date) || "-"
|
||||||
}}</span>
|
}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@ -21,23 +21,24 @@
|
|||||||
|
|
||||||
<div class="form-section mb-4">
|
<div class="form-section mb-4">
|
||||||
<h5 class="mb-3">Contenu du message</h5>
|
<h5 class="mb-3">Contenu du message</h5>
|
||||||
<webmailing-body-input
|
<webmailing-body-input v-model="formData.body" @blur="validateBody" />
|
||||||
v-model="formData.body"
|
|
||||||
@blur="validateBody"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section mb-4">
|
<div class="form-section mb-4">
|
||||||
<h5 class="mb-3">Pièces jointes</h5>
|
<h5 class="mb-3">Pièces jointes</h5>
|
||||||
<webmailing-attachment
|
<webmailing-attachment @files-selected="handleFilesSelected" />
|
||||||
@files-selected="handleFilesSelected"
|
|
||||||
/>
|
|
||||||
<div v-if="formData.attachments.length > 0" class="mt-3">
|
<div v-if="formData.attachments.length > 0" class="mt-3">
|
||||||
<h6>Fichiers sélectionnés:</h6>
|
<h6>Fichiers sélectionnés:</h6>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li v-for="file in formData.attachments" :key="file.name" class="mb-2">
|
<li
|
||||||
|
v-for="file in formData.attachments"
|
||||||
|
:key="file.name"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
<span class="badge bg-info">{{ file.name }}</span>
|
<span class="badge bg-info">{{ file.name }}</span>
|
||||||
<small class="ms-2 text-muted">({{ formatFileSize(file.size) }})</small>
|
<small class="ms-2 text-muted"
|
||||||
|
>({{ formatFileSize(file.size) }})</small
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +130,7 @@ const formatFileSize = (bytes) => {
|
|||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,10 @@
|
|||||||
<button class="btn btn-sm btn-info" @click="viewEmail(email.id)">
|
<button class="btn btn-sm btn-info" @click="viewEmail(email.id)">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-danger ms-2" @click="deleteEmail(email.id)">
|
<button
|
||||||
|
class="btn btn-sm btn-danger ms-2"
|
||||||
|
@click="deleteEmail(email.id)"
|
||||||
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -158,7 +158,6 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const filteredActivities = computed(() => {
|
const filteredActivities = computed(() => {
|
||||||
if (activeFilter.value === "all") {
|
if (activeFilter.value === "all") {
|
||||||
return activities.value;
|
return activities.value;
|
||||||
@ -167,11 +166,22 @@ const filteredActivities = computed(() => {
|
|||||||
return activities.value.filter((a) => {
|
return activities.value.filter((a) => {
|
||||||
const type = a.event_type;
|
const type = a.event_type;
|
||||||
switch (activeFilter.value) {
|
switch (activeFilter.value) {
|
||||||
case 'call': return type === 'call';
|
case "call":
|
||||||
case 'email': return ['email_sent', 'email_received'].includes(type);
|
return type === "call";
|
||||||
case 'invoice': return ['invoice_created', 'invoice_sent', 'invoice_paid'].includes(type);
|
case "email":
|
||||||
case 'file': return ['file_uploaded', 'attachment_sent', 'attachment_received'].includes(type);
|
return ["email_sent", "email_received"].includes(type);
|
||||||
default: return false;
|
case "invoice":
|
||||||
|
return ["invoice_created", "invoice_sent", "invoice_paid"].includes(
|
||||||
|
type
|
||||||
|
);
|
||||||
|
case "file":
|
||||||
|
return [
|
||||||
|
"file_uploaded",
|
||||||
|
"attachment_sent",
|
||||||
|
"attachment_received",
|
||||||
|
].includes(type);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,13 +9,15 @@
|
|||||||
<!-- Toggle Is Parent -->
|
<!-- Toggle Is Parent -->
|
||||||
<div class="form-check form-switch d-inline-block ms-auto">
|
<div class="form-check form-switch d-inline-block ms-auto">
|
||||||
<input
|
<input
|
||||||
|
id="isParentToggle"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isParentToggle"
|
|
||||||
:checked="client.is_parent"
|
:checked="client.is_parent"
|
||||||
@change="toggleParentStatus"
|
@change="toggleParentStatus"
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="isParentToggle">Compte Parent</label>
|
<label class="form-check-label" for="isParentToggle"
|
||||||
|
>Compte Parent</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<!-- Add Child Button -->
|
<!-- Add Child Button -->
|
||||||
<soft-button
|
<soft-button
|
||||||
@ -34,7 +36,8 @@
|
|||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<div v-if="!client.is_parent" class="text-center py-4">
|
<div v-if="!client.is_parent" class="text-center py-4">
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Ce client n'est pas défini comme compte parent. Activez l'option ci-dessus pour gérer des sous-comptes.
|
Ce client n'est pas défini comme compte parent. Activez l'option
|
||||||
|
ci-dessus pour gérer des sous-comptes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -50,7 +53,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="list-group">
|
<ul v-else class="list-group">
|
||||||
<li v-for="child in children" :key="child.id" class="list-group-item border-0 d-flex justify-content-between ps-0 mb-2 border-radius-lg">
|
<li
|
||||||
|
v-for="child in children"
|
||||||
|
:key="child.id"
|
||||||
|
class="list-group-item border-0 d-flex justify-content-between ps-0 mb-2 border-radius-lg"
|
||||||
|
>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-avatar
|
<soft-avatar
|
||||||
:img="getAvatar(child.name)"
|
:img="getAvatar(child.name)"
|
||||||
@ -65,10 +72,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center text-sm">
|
<div class="d-flex align-items-center text-sm">
|
||||||
<button class="btn btn-link text-dark text-sm mb-0 px-0 ms-4" @click="goToClient(child.id)">
|
<button
|
||||||
|
class="btn btn-link text-dark text-sm mb-0 px-0 ms-4"
|
||||||
|
@click="goToClient(child.id)"
|
||||||
|
>
|
||||||
<i class="fas fa-eye text-lg me-1"></i> Voir
|
<i class="fas fa-eye text-lg me-1"></i> Voir
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-link text-danger text-gradient px-3 mb-0" @click="confirmRemoveChild(child)">
|
<button
|
||||||
|
class="btn btn-link text-danger text-gradient px-3 mb-0"
|
||||||
|
@click="confirmRemoveChild(child)"
|
||||||
|
>
|
||||||
<i class="far fa-trash-alt me-2"></i> Détacher
|
<i class="far fa-trash-alt me-2"></i> Détacher
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +92,7 @@
|
|||||||
|
|
||||||
<AddChildClientModal
|
<AddChildClientModal
|
||||||
:show="showAddModal"
|
:show="showAddModal"
|
||||||
:exclude-ids="[client.id, client.parent_id, ...children.map(c => c.id)]"
|
:exclude-ids="[client.id, client.parent_id, ...children.map((c) => c.id)]"
|
||||||
@close="showAddModal = false"
|
@close="showAddModal = false"
|
||||||
@add="handleAddChild"
|
@add="handleAddChild"
|
||||||
/>
|
/>
|
||||||
@ -92,8 +105,8 @@ import SoftButton from "@/components/SoftButton.vue";
|
|||||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||||
import AddChildClientModal from "@/components/Organism/CRM/client/AddChildClientModal.vue";
|
import AddChildClientModal from "@/components/Organism/CRM/client/AddChildClientModal.vue";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from "vue-router";
|
||||||
import Swal from 'sweetalert2';
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
client: {
|
client: {
|
||||||
@ -102,7 +115,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update-client']);
|
const emit = defineEmits(["update-client"]);
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const children = ref([]);
|
const children = ref([]);
|
||||||
@ -126,10 +139,12 @@ onMounted(() => {
|
|||||||
fetchChildren();
|
fetchChildren();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.client.is_parent, (newVal) => {
|
watch(
|
||||||
|
() => props.client.is_parent,
|
||||||
|
(newVal) => {
|
||||||
if (newVal) fetchChildren();
|
if (newVal) fetchChildren();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/* eslint-disable require-atomic-updates */
|
/* eslint-disable require-atomic-updates */
|
||||||
const toggleParentStatus = async (e) => {
|
const toggleParentStatus = async (e) => {
|
||||||
@ -143,44 +158,47 @@ const toggleParentStatus = async (e) => {
|
|||||||
await clientStore.updateClient({
|
await clientStore.updateClient({
|
||||||
id: props.client.id,
|
id: props.client.id,
|
||||||
name: props.client.name,
|
name: props.client.name,
|
||||||
is_parent: isChecked
|
is_parent: isChecked,
|
||||||
});
|
});
|
||||||
// The parent component should react to store changes if it watches it, or we emit updated
|
// The parent component should react to store changes if it watches it, or we emit updated
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Revert on error
|
// Revert on error
|
||||||
e.target.checked = !isChecked;
|
e.target.checked = !isChecked;
|
||||||
Swal.fire('Erreur', 'Impossible de mettre à jour le statut parent.', 'error');
|
Swal.fire(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de mettre à jour le statut parent.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddChild = async (selectedClient) => {
|
const handleAddChild = async (selectedClient) => {
|
||||||
try {
|
try {
|
||||||
await clientStore.addChildClient(props.client.id, selectedClient.id);
|
await clientStore.addChildClient(props.client.id, selectedClient.id);
|
||||||
Swal.fire('Succès', 'Le client a été ajouté comme sous-compte.', 'success');
|
Swal.fire("Succès", "Le client a été ajouté comme sous-compte.", "success");
|
||||||
fetchChildren();
|
fetchChildren();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Swal.fire('Erreur', "Impossible d'ajouter le sous-compte.", 'error');
|
Swal.fire("Erreur", "Impossible d'ajouter le sous-compte.", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const confirmRemoveChild = async (child) => {
|
const confirmRemoveChild = async (child) => {
|
||||||
const result = await Swal.fire({
|
const result = await Swal.fire({
|
||||||
title: 'Confirmer le détachement',
|
title: "Confirmer le détachement",
|
||||||
text: `Voulez-vous vraiment détacher ${child.name} ?`,
|
text: `Voulez-vous vraiment détacher ${child.name} ?`,
|
||||||
icon: 'warning',
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: 'Oui, détacher',
|
confirmButtonText: "Oui, détacher",
|
||||||
cancelButtonText: 'Annuler'
|
cancelButtonText: "Annuler",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
try {
|
try {
|
||||||
await clientStore.removeChildClient(props.client.id, child.id);
|
await clientStore.removeChildClient(props.client.id, child.id);
|
||||||
Swal.fire('Détaché!', 'Le client a été détaché.', 'success');
|
Swal.fire("Détaché!", "Le client a été détaché.", "success");
|
||||||
children.value = children.value.filter(c => c.id !== child.id);
|
children.value = children.value.filter((c) => c.id !== child.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Swal.fire('Erreur', "Impossible de détacher le client.", 'error');
|
Swal.fire("Erreur", "Impossible de détacher le client.", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -194,5 +212,4 @@ const getAvatar = (name) => {
|
|||||||
// placeholder logic, replace with actual avatar logic or component usage
|
// placeholder logic, replace with actual avatar logic or component usage
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -20,7 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading" class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm" style="z-index: 1000; top: 100%;">
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm"
|
||||||
|
style="z-index: 1000; top: 100%"
|
||||||
|
>
|
||||||
<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>
|
||||||
@ -30,7 +34,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="results.length > 0 && showResults"
|
v-else-if="results.length > 0 && showResults"
|
||||||
class="list-group position-absolute w-100 mt-1 shadow-lg"
|
class="list-group position-absolute w-100 mt-1 shadow-lg"
|
||||||
style="z-index: 1000; max-height: 300px; overflow-y: auto;"
|
style="z-index: 1000; max-height: 300px; overflow-y: auto"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-for="client in results"
|
v-for="client in results"
|
||||||
@ -48,7 +52,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<span class="font-weight-bold text-sm">{{ client.name }}</span>
|
<span class="font-weight-bold text-sm">{{ client.name }}</span>
|
||||||
<span class="text-xs text-muted">{{ client.email || 'Pas d\'email' }}</span>
|
<span class="text-xs text-muted">{{
|
||||||
|
client.email || "Pas d'email"
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -57,7 +63,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="searchQuery && !loading && showResults"
|
v-else-if="searchQuery && !loading && showResults"
|
||||||
class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted"
|
class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted"
|
||||||
style="z-index: 1000;"
|
style="z-index: 1000"
|
||||||
>
|
>
|
||||||
Aucun client trouvé.
|
Aucun client trouvé.
|
||||||
</div>
|
</div>
|
||||||
@ -72,8 +78,8 @@ import { useClientStore } from "@/stores/clientStore";
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
excludeIds: {
|
excludeIds: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["select"]);
|
const emit = defineEmits(["select"]);
|
||||||
@ -100,7 +106,7 @@ const handleInput = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await clientStore.searchClients(searchQuery.value);
|
const res = await clientStore.searchClients(searchQuery.value);
|
||||||
// Filter out excluded IDs (e.g. self, parent)
|
// Filter out excluded IDs (e.g. self, parent)
|
||||||
results.value = res.filter(c => !props.excludeIds.includes(c.id));
|
results.value = res.filter((c) => !props.excludeIds.includes(c.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
results.value = [];
|
results.value = [];
|
||||||
|
|||||||
@ -22,10 +22,7 @@
|
|||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div
|
<div v-if="fieldErrors.client_category_id" class="invalid-feedback">
|
||||||
v-if="fieldErrors.client_category_id"
|
|
||||||
class="invalid-feedback"
|
|
||||||
>
|
|
||||||
{{ errorMessage("client_category_id") }}
|
{{ errorMessage("client_category_id") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -215,7 +215,7 @@ watch(
|
|||||||
(newErrors) => {
|
(newErrors) => {
|
||||||
fieldErrors.value = { ...newErrors };
|
fieldErrors.value = { ...newErrors };
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Watch for success from parent
|
// Watch for success from parent
|
||||||
@ -225,7 +225,7 @@ watch(
|
|||||||
if (newSuccess) {
|
if (newSuccess) {
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
|||||||
@ -294,7 +294,7 @@ watch(
|
|||||||
(newErrors) => {
|
(newErrors) => {
|
||||||
fieldErrors.value = { ...newErrors };
|
fieldErrors.value = { ...newErrors };
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Watch for success from parent
|
// Watch for success from parent
|
||||||
@ -304,7 +304,7 @@ watch(
|
|||||||
if (newSuccess) {
|
if (newSuccess) {
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
|||||||
@ -240,8 +240,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input
|
<input
|
||||||
id="isDefaultCheckbox"
|
id="isDefaultCheckbox"
|
||||||
|
|||||||
@ -1,16 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4 fournisseur-detail-shell">
|
||||||
<div class="row mb-4">
|
<div class="card border-0 mb-4 hero-shell">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div
|
||||||
|
class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-white mb-1">
|
||||||
|
<slot name="page-title">Détail fournisseur</slot>
|
||||||
|
</h4>
|
||||||
|
<p class="text-white-50 mb-0">
|
||||||
|
<slot name="page-subtitle"
|
||||||
|
>Consultez et gérez toutes les informations du
|
||||||
|
fournisseur.</slot
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||||
|
<slot name="header-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<slot name="summary-cards" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
<slot name="button-return" />
|
<slot name="button-return" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3">
|
<div class="row g-4">
|
||||||
|
<div class="col-xl-3 col-lg-4">
|
||||||
<slot name="fournisseur-detail-sidebar" />
|
<slot name="fournisseur-detail-sidebar" />
|
||||||
<slot name="file-input" />
|
<slot name="file-input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-9 mt-lg-0 mt-4">
|
<div class="col-xl-9 col-lg-8 mt-lg-0 mt-2">
|
||||||
<slot name="fournisseur-detail-content" />
|
<slot name="fournisseur-detail-content" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero-shell {
|
||||||
|
background: linear-gradient(135deg, #344767 0%, #5e72e4 100%);
|
||||||
|
box-shadow: 0 10px 30px rgba(52, 71, 103, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,19 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4 fournisseur-shell">
|
||||||
<div class="d-sm-flex justify-content-between">
|
<div class="card border-0 mb-4 hero-shell">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div
|
||||||
|
class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-white mb-1">
|
||||||
|
<slot name="page-title">Gestion des fournisseurs</slot>
|
||||||
|
</h4>
|
||||||
|
<p class="text-white-50 mb-0">
|
||||||
|
<slot name="page-subtitle"
|
||||||
|
>Pilotez vos fournisseurs et centralisez leurs
|
||||||
|
informations.</slot
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<slot name="header-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<slot name="summary-cards" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 content-shell">
|
||||||
|
<div class="card-body p-3 p-md-4">
|
||||||
|
<div
|
||||||
|
class="d-sm-flex justify-content-between align-items-center gap-2 action-row"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<slot name="fournisseur-new-action"></slot>
|
<slot name="fournisseur-new-action"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||||
<div class="dropdown d-inline">
|
<div class="dropdown d-inline">
|
||||||
<slot name="select-filter"></slot>
|
<slot name="select-filter"></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot name="fournisseur-other-action"></slot>
|
<slot name="fournisseur-other-action"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
<div class="mt-3 table-shell">
|
||||||
<div class="card mt-4">
|
|
||||||
<slot name="fournisseur-table"></slot>
|
<slot name="fournisseur-table"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -21,3 +51,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script></script>
|
<script></script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero-shell {
|
||||||
|
background: linear-gradient(135deg, #344767 0%, #5e72e4 100%);
|
||||||
|
box-shadow: 0 10px 30px rgba(52, 71, 103, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-shell {
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-shell {
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<div class="card-header p-3 pb-0">
|
<div class="card-header p-3 pb-0">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
<hr class="horizontal dark my-3">
|
<hr class="horizontal dark my-3" />
|
||||||
<div class="card-body p-3 pt-0">
|
<div class="card-body p-3 pt-0">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<slot name="summary"></slot>
|
<slot name="summary"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="horizontal dark mt-4 mb-3">
|
<hr class="horizontal dark mt-4 mb-3" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="planning-container p-2 p-md-4">
|
<div class="planning-container p-2 p-md-4">
|
||||||
<div class="container-max mx-auto">
|
<div class="container-max mx-auto h-100">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4 gap-3">
|
<div
|
||||||
|
class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4 gap-3"
|
||||||
|
>
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -11,7 +13,7 @@
|
|||||||
<slot name="view-toggles"></slot>
|
<slot name="view-toggles"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper h-100">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Sidebar & Legend Column (Left on desktop) -->
|
<!-- Sidebar & Legend Column (Left on desktop) -->
|
||||||
<!-- <div class="col-12 col-xl-3 order-2 order-xl-1">
|
<!-- <div class="col-12 col-xl-3 order-2 order-xl-1">
|
||||||
@ -44,11 +46,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container-max {
|
.container-max {
|
||||||
|
width: 100%;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
|
min-height: calc(100vh - 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: calc(100vh - 12rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-3 {
|
.gap-3 {
|
||||||
|
|||||||
@ -477,17 +477,20 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: "/fournisseurs/factures",
|
path: "/fournisseurs/factures",
|
||||||
name: "Factures fournisseurs",
|
name: "Factures fournisseurs",
|
||||||
component: () => import("@/views/pages/Fournisseurs/FactureFournisseurList.vue"),
|
component: () =>
|
||||||
|
import("@/views/pages/Fournisseurs/FactureFournisseurList.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/fournisseurs/factures/new",
|
path: "/fournisseurs/factures/new",
|
||||||
name: "Nouvelle Facture Fournisseur",
|
name: "Nouvelle Facture Fournisseur",
|
||||||
component: () => import("@/views/pages/Fournisseurs/NewFactureFournisseur.vue"),
|
component: () =>
|
||||||
|
import("@/views/pages/Fournisseurs/NewFactureFournisseur.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/fournisseurs/factures/:id",
|
path: "/fournisseurs/factures/:id",
|
||||||
name: "Facture Fournisseur Details",
|
name: "Facture Fournisseur Details",
|
||||||
component: () => import("@/views/pages/Fournisseurs/FactureFournisseurDetail.vue"),
|
component: () =>
|
||||||
|
import("@/views/pages/Fournisseurs/FactureFournisseurDetail.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/fournisseurs/statistiques",
|
path: "/fournisseurs/statistiques",
|
||||||
|
|||||||
@ -131,7 +131,9 @@ export const AvoirService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteAvoir(id: number): Promise<{ success: boolean; message: string }> {
|
async deleteAvoir(
|
||||||
|
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/avoirs/${id}`,
|
url: `/api/avoirs/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
|
|||||||
@ -27,7 +27,8 @@ export interface CreateClientGroupPayload {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateClientGroupPayload extends Partial<CreateClientGroupPayload> {
|
export interface UpdateClientGroupPayload
|
||||||
|
extends Partial<CreateClientGroupPayload> {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +65,9 @@ export const ClientGroupService = {
|
|||||||
/**
|
/**
|
||||||
* Create a new client group
|
* Create a new client group
|
||||||
*/
|
*/
|
||||||
async createClientGroup(payload: CreateClientGroupPayload): Promise<ClientGroupResponse> {
|
async createClientGroup(
|
||||||
|
payload: CreateClientGroupPayload
|
||||||
|
): Promise<ClientGroupResponse> {
|
||||||
const response = await request<ClientGroupResponse>({
|
const response = await request<ClientGroupResponse>({
|
||||||
url: "/api/client-groups",
|
url: "/api/client-groups",
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -77,7 +80,9 @@ export const ClientGroupService = {
|
|||||||
/**
|
/**
|
||||||
* Update an existing client group
|
* Update an existing client group
|
||||||
*/
|
*/
|
||||||
async updateClientGroup(payload: UpdateClientGroupPayload): Promise<ClientGroupResponse> {
|
async updateClientGroup(
|
||||||
|
payload: UpdateClientGroupPayload
|
||||||
|
): Promise<ClientGroupResponse> {
|
||||||
const { id, ...updateData } = payload;
|
const { id, ...updateData } = payload;
|
||||||
|
|
||||||
const response = await request<ClientGroupResponse>({
|
const response = await request<ClientGroupResponse>({
|
||||||
|
|||||||
@ -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;
|
||||||
@ -22,7 +22,7 @@ export interface SupplierInvoice {
|
|||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
invoice_date: string;
|
invoice_date: string;
|
||||||
due_date: string | null;
|
due_date: string | null;
|
||||||
status: 'brouillon' | 'en_attente' | 'payee' | 'annulee';
|
status: "brouillon" | "en_attente" | "payee" | "annulee";
|
||||||
currency: string;
|
currency: string;
|
||||||
total_ht: number;
|
total_ht: number;
|
||||||
total_tva: number;
|
total_tva: number;
|
||||||
@ -70,7 +70,8 @@ export interface CreateSupplierInvoicePayload {
|
|||||||
lines?: CreateSupplierInvoiceLinePayload[];
|
lines?: CreateSupplierInvoiceLinePayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateSupplierInvoicePayload extends Partial<CreateSupplierInvoicePayload> {
|
export interface UpdateSupplierInvoicePayload
|
||||||
|
extends Partial<CreateSupplierInvoicePayload> {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +99,9 @@ export const SupplierInvoiceService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createSupplierInvoice(payload: CreateSupplierInvoicePayload): Promise<SupplierInvoiceResponse> {
|
async createSupplierInvoice(
|
||||||
|
payload: CreateSupplierInvoicePayload
|
||||||
|
): Promise<SupplierInvoiceResponse> {
|
||||||
const response = await request<SupplierInvoiceResponse>({
|
const response = await request<SupplierInvoiceResponse>({
|
||||||
url: "/api/supplier-invoices",
|
url: "/api/supplier-invoices",
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -107,7 +110,9 @@ export const SupplierInvoiceService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateSupplierInvoice(payload: UpdateSupplierInvoicePayload): Promise<SupplierInvoiceResponse> {
|
async updateSupplierInvoice(
|
||||||
|
payload: UpdateSupplierInvoicePayload
|
||||||
|
): Promise<SupplierInvoiceResponse> {
|
||||||
const { id, ...updateData } = payload;
|
const { id, ...updateData } = payload;
|
||||||
const response = await request<SupplierInvoiceResponse>({
|
const response = await request<SupplierInvoiceResponse>({
|
||||||
url: `/api/supplier-invoices/${id}`,
|
url: `/api/supplier-invoices/${id}`,
|
||||||
@ -117,7 +122,9 @@ export const SupplierInvoiceService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteSupplierInvoice(id: number): Promise<{ success: boolean; message: string }> {
|
async deleteSupplierInvoice(
|
||||||
|
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/supplier-invoices/${id}`,
|
url: `/api/supplier-invoices/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
@ -125,7 +132,9 @@ export const SupplierInvoiceService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getByFournisseur(fournisseurId: number): Promise<SupplierInvoiceListResponse> {
|
async getByFournisseur(
|
||||||
|
fournisseurId: number
|
||||||
|
): Promise<SupplierInvoiceListResponse> {
|
||||||
const response = await request<SupplierInvoiceListResponse>({
|
const response = await request<SupplierInvoiceListResponse>({
|
||||||
url: `/api/fournisseurs/${fournisseurId}/supplier-invoices`,
|
url: `/api/fournisseurs/${fournisseurId}/supplier-invoices`,
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|||||||
@ -61,7 +61,9 @@ export const TvaRateService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteTvaRate(id: number): Promise<{ success: boolean; message: string }> {
|
async deleteTvaRate(
|
||||||
|
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/tva-rates/${id}`,
|
url: `/api/tva-rates/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -146,10 +146,7 @@ export const useAvoirStore = defineStore("avoir", () => {
|
|||||||
avoirs.value[index] = updatedAvoir;
|
avoirs.value[index] = updatedAvoir;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (currentAvoir.value && currentAvoir.value.id === updatedAvoir.id) {
|
||||||
currentAvoir.value &&
|
|
||||||
currentAvoir.value.id === updatedAvoir.id
|
|
||||||
) {
|
|
||||||
setCurrentAvoir(updatedAvoir);
|
setCurrentAvoir(updatedAvoir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,9 +170,7 @@ export const useAvoirStore = defineStore("avoir", () => {
|
|||||||
try {
|
try {
|
||||||
const response = await AvoirService.deleteAvoir(id);
|
const response = await AvoirService.deleteAvoir(id);
|
||||||
|
|
||||||
avoirs.value = avoirs.value.filter(
|
avoirs.value = avoirs.value.filter((avoir) => avoir.id !== id);
|
||||||
(avoir) => avoir.id !== id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentAvoir.value && currentAvoir.value.id === id) {
|
if (currentAvoir.value && currentAvoir.value.id === id) {
|
||||||
setCurrentAvoir(null);
|
setCurrentAvoir(null);
|
||||||
|
|||||||
@ -79,7 +79,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
return response;
|
return response;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message || err.message || "Failed to fetch client groups";
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch client groups";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -100,7 +102,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message || err.message || "Failed to fetch client group";
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch client group";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -123,7 +127,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message || err.message || "Failed to create client group";
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to create client group";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -151,14 +157,19 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update current group if it's the one being edited
|
// Update current group if it's the one being edited
|
||||||
if (currentClientGroup.value && currentClientGroup.value.id === updatedGroup.id) {
|
if (
|
||||||
|
currentClientGroup.value &&
|
||||||
|
currentClientGroup.value.id === updatedGroup.id
|
||||||
|
) {
|
||||||
setCurrentClientGroup(updatedGroup);
|
setCurrentClientGroup(updatedGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedGroup;
|
return updatedGroup;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message || err.message || "Failed to update client group";
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to update client group";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -177,7 +188,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
const response = await ClientGroupService.deleteClientGroup(id);
|
const response = await ClientGroupService.deleteClientGroup(id);
|
||||||
|
|
||||||
// Remove from the groups list
|
// Remove from the groups list
|
||||||
clientGroups.value = clientGroups.value.filter((group) => group.id !== id);
|
clientGroups.value = clientGroups.value.filter(
|
||||||
|
(group) => group.id !== id
|
||||||
|
);
|
||||||
|
|
||||||
// Clear current group if it's the one being deleted
|
// Clear current group if it's the one being deleted
|
||||||
if (currentClientGroup.value && currentClientGroup.value.id === id) {
|
if (currentClientGroup.value && currentClientGroup.value.id === id) {
|
||||||
@ -187,7 +200,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
return response;
|
return response;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message || err.message || "Failed to delete client group";
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to delete client group";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user