183 lines
4.9 KiB
Vue
183 lines
4.9 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>
|
|
<quote-detail-template v-else-if="quote">
|
|
<template #header>
|
|
<quote-header
|
|
:reference="quote.reference"
|
|
:date="quote.quote_date"
|
|
:code="quote.reference"
|
|
/>
|
|
</template>
|
|
|
|
<template #lines>
|
|
<quote-lines-table :lines="quote.lines" />
|
|
</template>
|
|
|
|
<template #timeline>
|
|
<quote-timeline :history="quote.history" />
|
|
</template>
|
|
|
|
<template #billing>
|
|
<quote-billing-info
|
|
:client-name="quote.client ? quote.client.name : 'Client inconnu'"
|
|
:client-email="quote.client ? quote.client.email : ''"
|
|
:client-phone="quote.client ? quote.client.phone : ''"
|
|
/>
|
|
</template>
|
|
|
|
<template #summary>
|
|
<quote-summary
|
|
:ht="quote.total_ht"
|
|
:tva="quote.total_tva"
|
|
:ttc="quote.total_ttc"
|
|
/>
|
|
</template>
|
|
|
|
<template #actions>
|
|
<div class="d-flex justify-content-end">
|
|
<div class="position-relative d-inline-block me-2">
|
|
<soft-button
|
|
color="secondary"
|
|
variant="gradient"
|
|
@click="dropdownOpen = !dropdownOpen"
|
|
>
|
|
{{ getStatusLabel(quote.status) }}
|
|
<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 === quote.status }"
|
|
href="javascript:;"
|
|
@click="changeStatus(status); dropdownOpen = false;"
|
|
>
|
|
{{ getStatusLabel(status) }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</quote-detail-template>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, defineProps } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { useQuoteStore } from "@/stores/quoteStore";
|
|
import { useNotificationStore } from "@/stores/notification";
|
|
import QuoteDetailTemplate from "@/components/templates/Quote/QuoteDetailTemplate.vue";
|
|
import QuoteHeader from "@/components/molecules/Quote/QuoteHeader.vue";
|
|
import QuoteTimeline from "@/components/molecules/Quote/QuoteTimeline.vue";
|
|
import QuoteBillingInfo from "@/components/molecules/Quote/QuoteBillingInfo.vue";
|
|
import QuoteSummary from "@/components/molecules/Quote/QuoteSummary.vue";
|
|
import QuoteLinesTable from "@/components/molecules/Quote/QuoteLinesTable.vue";
|
|
import SoftButton from "@/components/SoftButton.vue";
|
|
|
|
const props = defineProps({
|
|
quoteId: {
|
|
type: [String, Number],
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const router = useRouter();
|
|
const quoteStore = useQuoteStore();
|
|
const notificationStore = useNotificationStore();
|
|
const quote = ref(null);
|
|
const loading = ref(true);
|
|
const error = ref(null);
|
|
const dropdownOpen = ref(false);
|
|
|
|
onMounted(async () => {
|
|
loading.value = true;
|
|
try {
|
|
const fetchedQuote = await quoteStore.fetchQuote(props.quoteId);
|
|
quote.value = fetchedQuote;
|
|
} catch (e) {
|
|
error.value = "Impossible de charger le devis.";
|
|
console.error(e);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
|
|
const goBack = () => {
|
|
router.back();
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return "-";
|
|
return new Date(dateString).toLocaleDateString("fr-FR");
|
|
};
|
|
|
|
const availableStatuses = [
|
|
"brouillon",
|
|
"envoye",
|
|
"accepte",
|
|
"refuse",
|
|
"expire",
|
|
"annule",
|
|
];
|
|
|
|
const getStatusLabel = (status) => {
|
|
const labels = {
|
|
brouillon: "Brouillon",
|
|
envoye: "Envoyé",
|
|
accepte: "Accepté",
|
|
refuse: "Refusé",
|
|
expire: "Expiré",
|
|
annule: "Annulé",
|
|
};
|
|
return labels[status] || status;
|
|
};
|
|
|
|
/* eslint-disable require-atomic-updates */
|
|
const changeStatus = async (newStatus) => {
|
|
if (!quote.value?.id) return;
|
|
|
|
// Capture the current quote ID to prevent race conditions
|
|
const currentQuoteId = quote.value.id;
|
|
|
|
try {
|
|
loading.value = true;
|
|
const updated = await quoteStore.updateQuote({
|
|
id: currentQuoteId,
|
|
status: newStatus,
|
|
});
|
|
|
|
// Only update if we're still viewing the same quote
|
|
if (quote.value?.id === currentQuoteId) {
|
|
quote.value = updated;
|
|
|
|
// Show success notification
|
|
notificationStore.success(
|
|
'Statut mis à jour',
|
|
`Le devis est maintenant "${getStatusLabel(newStatus)}"`,
|
|
3000
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to update status", e);
|
|
notificationStore.error(
|
|
'Erreur',
|
|
'Impossible de mettre à jour le statut',
|
|
3000
|
|
);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
</script> |