New-Thanasoft/thanasoft-front/src/components/Organism/Stock/ReceptionDetailPresentation.vue

385 lines
9.8 KiB
Vue

<template>
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else-if="error" class="text-center py-5 text-danger">
{{ error }}
</div>
<div v-else-if="goodsReceipt" class="container-fluid py-4">
<div class="odoo-toolbar">
<div class="statusbar-wrapper">
<button
v-for="status in availableStatuses"
:key="status"
type="button"
class="status-step"
:class="{ active: status === goodsReceipt.status }"
:disabled="true"
>
<i :class="getStatusIcon(status)"></i>
<span>{{ getStatusLabel(status) }}</span>
</button>
</div>
<div class="toolbar-right">
<soft-button
color="success"
variant="outline"
class="btn-toolbar btn-sm"
:disabled="!canValidate || isUpdatingStatus"
@click="handleValidate"
>
<i class="fas fa-check me-2"></i>
Valider réception
</soft-button>
<soft-button
color="warning"
variant="outline"
class="btn-toolbar btn-sm"
:disabled="!canSetDraft || isUpdatingStatus"
@click="handleSetDraft"
>
<i class="fas fa-undo me-2"></i>
Remettre en brouillon
</soft-button>
<soft-button
color="secondary"
variant="outline"
class="btn-toolbar btn-sm"
@click="handleBack"
>
<i class="fas fa-arrow-left me-2"></i>
Retour
</soft-button>
<soft-button
color="info"
variant="outline"
class="btn-toolbar btn-sm"
@click="handleEdit"
>
<i class="fas fa-edit me-2"></i>
Modifier
</soft-button>
<soft-button
color="danger"
variant="outline"
class="btn-toolbar btn-sm"
:disabled="isUpdatingStatus"
@click="handleDelete"
>
<i class="fas fa-trash me-2"></i>
Supprimer
</soft-button>
</div>
</div>
<div class="commande-detail mt-3">
<div class="form-section">
<div class="section-title">
<i class="fas fa-truck-loading"></i>
Informations générales
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Numéro réception</label>
<div class="info-value">{{ goodsReceipt.receipt_number }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Date réception</label>
<div class="info-value">{{ formatDate(goodsReceipt.receipt_date) }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Statut</label>
<div class="status-badge" :class="getStatusClass(goodsReceipt.status)">
<i :class="getStatusIcon(goodsReceipt.status)"></i>
{{ getStatusLabel(goodsReceipt.status) }}
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Commande fournisseur</label>
<div class="info-value">
{{ goodsReceipt.purchase_order?.po_number || goodsReceipt.purchase_order_id }}
</div>
</div>
<div class="col-md-6">
<label class="form-label">Entrepôt</label>
<div class="info-value">{{ goodsReceipt.warehouse?.name || "-" }}</div>
</div>
</div>
</div>
<div class="form-section">
<div class="section-title">
<i class="fas fa-boxes"></i>
Lignes de réception
</div>
<div class="table-responsive">
<table class="table table-flush">
<thead class="thead-light">
<tr>
<th>Produit</th>
<th>Conditionnement</th>
<th>Colis</th>
<th>Unités</th>
<th>Prix Unitaire</th>
<th>TVA</th>
</tr>
</thead>
<tbody>
<tr v-for="line in receiptLines" :key="line.id">
<td>
<div class="d-flex flex-column">
<span class="text-sm fw-bold">{{ line.product?.nom || `Produit #${line.product_id}` }}</span>
<span class="text-xs text-secondary">{{ line.product?.reference || "-" }}</span>
</div>
</td>
<td class="text-sm">{{ line.packaging?.name || "Unité" }}</td>
<td class="text-sm">{{ line.packages_qty_received || "-" }}</td>
<td class="text-sm">{{ line.units_qty_received || "-" }}</td>
<td class="text-sm">{{ line.unit_price ? formatCurrency(line.unit_price) : "-" }}</td>
<td class="text-sm">
{{ line.tva_rate ? `${line.tva_rate.name} (${line.tva_rate.rate}%)` : "-" }}
</td>
</tr>
<tr v-if="receiptLines.length === 0">
<td colspan="6" class="text-center text-muted py-4">
Aucune ligne dans cette réception.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { storeToRefs } from "pinia";
import SoftButton from "@/components/SoftButton.vue";
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
const route = useRoute();
const router = useRouter();
const goodsReceiptStore = useGoodsReceiptStore();
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) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
});
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value || 0);
};
const getStatusLabel = (status) => {
const labels = {
draft: "Brouillon",
posted: "Validée",
};
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 = () => {
router.push("/stock/receptions");
};
const handleEdit = () => {
router.push(`/stock/receptions/${route.params.id}/edit`);
};
const handleDelete = async () => {
if (!confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
return;
}
try {
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
router.push("/stock/receptions");
} catch (e) {
console.error("Failed to delete goods receipt", e);
}
};
onMounted(async () => {
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
});
</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>