New-Thanasoft/thanasoft-front/src/components/Organism/Commande/CommandeDetailPresentation.vue
2026-01-29 16:44:31 +03:00

564 lines
13 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="commande" class="commande-detail">
<!-- Header Section -->
<div class="form-section">
<div class="section-title">
<i class="fas fa-file-invoice"></i>
Informations générales
</div>
<!-- Row 1: Commande Number, Date, Status -->
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Numéro commande</label>
<div class="info-value">{{ commande.number }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Date commande</label>
<div class="info-value">{{ formatDate(commande.date) }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Statut</label>
<div class="status-badge" :class="getStatusClass(commande.status)">
<i :class="getStatusIcon(commande.status)"></i>
{{ getStatusLabel(commande.status) }}
</div>
</div>
</div>
<!-- Row 2: Fournisseur, Contact -->
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Fournisseur</label>
<div class="info-value">{{ commande.supplierName }}</div>
</div>
<div class="col-md-6">
<label class="form-label">Contact</label>
<div class="info-value">{{ commande.supplierContact }}</div>
</div>
</div>
<!-- Row 3: Adresse livraison -->
<div class="mb-0">
<label class="form-label">Adresse livraison</label>
<div class="info-value">{{ commande.deliveryAddress || 'Non spécifiée' }}</div>
</div>
</div>
<!-- Articles Section -->
<div class="form-section">
<div class="section-header">
<div class="section-title">
<i class="fas fa-boxes"></i>
Articles commandés
</div>
</div>
<div class="lines-container">
<div
v-for="line in commande.lines"
:key="line.id"
class="line-item"
>
<div class="row g-2 align-items-center">
<div class="col-md-5">
<label class="form-label text-xs">Désignation</label>
<div class="line-designation">{{ line.designation }}</div>
</div>
<div class="col-md-2">
<label class="form-label text-xs">Quantité</label>
<div class="line-quantity">{{ line.quantity }}</div>
</div>
<div class="col-md-2">
<label class="form-label text-xs">Prix HT</label>
<div class="line-price">{{ formatCurrency(line.price_ht) }}</div>
</div>
<div class="col-md-3 d-flex flex-column align-items-end">
<label class="form-label text-xs">Total HT</label>
<span class="line-total">
{{ formatCurrency(line.total_ht) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Totals Section -->
<div class="totals-section">
<div class="totals-content">
<div class="total-row">
<span class="total-label">Total HT</span>
<span class="total-value">{{ formatCurrency(commande.total_ht) }}</span>
</div>
<div class="total-row">
<span class="total-label">TVA (20%)</span>
<span class="total-value">{{ formatCurrency(commande.total_tva) }}</span>
</div>
<div class="total-row total-final">
<span class="total-label">Total TTC</span>
<span class="total-amount">{{ formatCurrency(commande.total_ttc) }}</span>
</div>
</div>
</div>
<!-- Additional Info Section -->
<div class="form-section">
<div class="section-title">
<i class="fas fa-info-circle"></i>
Informations supplémentaires
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Adresse fournisseur</label>
<div class="info-value">{{ commande.supplierAddress }}</div>
</div>
<div class="col-md-6">
<label class="form-label">Date de création</label>
<div class="info-value">{{ formatDate(commande.date) }}</div>
</div>
</div>
<div v-if="commande.notes" class="mt-3">
<label class="form-label">Notes</label>
<div class="info-value notes-content">{{ commande.notes }}</div>
</div>
</div>
<!-- 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>
</template>
<script setup>
import { ref, defineProps, onMounted } from "vue";
import { useRouter } from "vue-router";
import SoftButton from "@/components/SoftButton.vue";
import { PurchaseOrderService } from "@/services/purchaseOrder";
import { useNotificationStore } from "@/stores/notification";
const props = defineProps({
commandeId: {
type: [String, Number],
required: true,
},
});
const router = useRouter();
const notificationStore = useNotificationStore();
const commande = ref(null);
const loading = ref(true);
const error = ref(null);
const dropdownOpen = ref(false);
const availableStatuses = ["brouillon", "confirmee", "livree", "facturee", "annulee"];
const formatDate = (dateString) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
});
};
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
confirmee: "Confirmée",
livree: "Livrée",
facturee: "Facturée",
annulee: "Annulée",
};
return labels[status] || status;
};
const getStatusIcon = (status) => {
const icons = {
brouillon: "fas fa-file-alt",
confirmee: "fas fa-check-circle",
livree: "fas fa-truck",
facturee: "fas fa-file-invoice-dollar",
annulee: "fas fa-times-circle",
};
return icons[status] || "fas fa-question-circle";
};
const getStatusClass = (status) => {
const classes = {
brouillon: "status-draft",
confirmee: "status-confirmed",
livree: "status-delivered",
facturee: "status-invoiced",
annulee: "status-cancelled",
};
return classes[status] || "";
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value || 0);
};
const fetchCommande = async () => {
loading.value = true;
error.value = null;
try {
const response = await PurchaseOrderService.getPurchaseOrder(props.commandeId);
const data = response.data;
// Map backend data to frontend structure
commande.value = {
id: data.id,
number: data.po_number,
status: data.status,
date: data.order_date,
total_ht: data.total_ht,
total_tva: data.total_tva,
total_ttc: data.total_ttc,
notes: data.notes,
deliveryAddress: data.delivery_address,
// Supplier mapping
supplierName: data.fournisseur?.name || "Inconnu",
supplierAddress: data.fournisseur ?
`${data.fournisseur.billing_address_line1 || ''} ${data.fournisseur.billing_city || ''}` :
"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: (data.lines || []).map(line => ({
id: line.id,
designation: line.description,
quantity: line.quantity,
price_ht: line.unit_price,
total_ht: line.total_ht
}))
};
} catch (err) {
console.error("Error fetching commande:", err);
error.value = "Impossible de charger les détails de la commande.";
notificationStore.error("Erreur", error.value);
} finally {
loading.value = false;
}
};
const changeStatus = async (newStatus) => {
try {
const payload = {
id: props.commandeId,
status: newStatus
};
await PurchaseOrderService.updatePurchaseOrder(payload);
commande.value.status = newStatus;
notificationStore.success("Succès", `Statut mis à jour : ${getStatusLabel(newStatus)}`);
} catch (err) {
console.error("Error updating status:", err);
notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
}
};
onMounted(() => {
fetchCommande();
});
</script>
<style scoped>
.commande-detail {
max-width: 1200px;
margin: 0 auto;
}
/* Form Sections */
.form-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title i {
color: #6c757d;
font-size: 0.9rem;
}
/* Form Labels */
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
.form-label.text-xs {
font-size: 0.8rem;
}
/* Info Value Display */
.info-value {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 0.625rem 1rem;
font-size: 0.9rem;
color: #212529;
font-weight: 500;
}
.notes-content {
white-space: pre-wrap;
line-height: 1.5;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
}
.status-badge i {
font-size: 0.9rem;
}
.status-draft {
background: #e9ecef;
color: #6c757d;
}
.status-confirmed {
background: #d1e7dd;
color: #0f5132;
}
.status-delivered {
background: #cff4fc;
color: #055160;
}
.status-invoiced {
background: #fff3cd;
color: #664d03;
}
.status-cancelled {
background: #f8d7da;
color: #842029;
}
/* Lines Container */
.lines-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.line-item {
background: #f8f9fa;
border-radius: 10px;
padding: 1rem;
border: 1px solid #e9ecef;
transition: box-shadow 0.2s, border-color 0.2s;
}
.line-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-color: #dee2e6;
}
.line-designation {
font-weight: 600;
color: #212529;
font-size: 0.9rem;
}
.line-quantity {
font-weight: 500;
color: #495057;
font-size: 0.9rem;
}
.line-price {
font-weight: 500;
color: #495057;
font-size: 0.9rem;
}
.line-total {
font-weight: 600;
color: #28a745;
font-size: 0.95rem;
white-space: nowrap;
}
/* Totals Section - Clean Design */
.totals-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.totals-content {
max-width: 350px;
margin-left: auto;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.total-row:last-child {
border-bottom: none;
}
.total-label {
font-size: 0.9rem;
color: #6c757d;
}
.total-value {
font-size: 0.95rem;
font-weight: 500;
color: #495057;
}
.total-final {
padding-top: 0.75rem;
margin-top: 0.5rem;
}
.total-final .total-label {
font-size: 1rem;
font-weight: 600;
color: #212529;
}
.total-amount {
font-size: 1.35rem;
font-weight: 700;
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 */
@media (max-width: 768px) {
.form-section {
padding: 1rem;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.totals-content {
max-width: 100%;
}
.action-buttons {
flex-direction: column-reverse;
}
.btn-status,
.btn-pdf {
width: 100%;
justify-content: center;
}
}
</style>