564 lines
13 KiB
Vue
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>
|