Simplify order statuses and manager actions

This commit is contained in:
Ruslan Bakiev
2026-04-04 14:01:46 +07:00
parent 2f828cd164
commit 9017555722
6 changed files with 216 additions and 142 deletions

View File

@@ -1,9 +1,6 @@
<script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
BlockOrderDocument,
CompleteOrderDocument,
ManagerFinalizeOrderDocument,
ManagerSetOrderOfferDocument,
OrderDetailDocument,
StartOrderWorkDocument,
@@ -29,15 +26,14 @@ const orderQuery = useQuery(OrderDetailDocument, () => ({
id: orderId.value,
}));
const finalizeMutation = useMutation(ManagerFinalizeOrderDocument);
const blockMutation = useMutation(BlockOrderDocument);
const startWorkMutation = useMutation(StartOrderWorkDocument);
const completeWorkMutation = useMutation(CompleteOrderDocument);
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
const itemPriceDrafts = reactive<Record<string, string>>({});
const deliveryTermsDraft = ref('');
const deliveryFeeDraft = ref('');
const autosavePending = ref(false);
let autosaveTimer: ReturnType<typeof setTimeout> | null = null;
const currentOrder = computed<ManagerOrderItem | null>(() =>
orderQuery.result.value?.order ?? null,
@@ -95,6 +91,40 @@ function draftLineTotal(item: ManagerOrderItem['items'][number]) {
const draftDeliveryTerms = computed(() => deliveryTermsDraft.value.trim() || currentOrder.value?.deliveryTerms || null);
const draftDeliveryFee = computed(() => parseMoneyDraft(deliveryFeeDraft.value) ?? currentOrder.value?.deliveryFee ?? null);
const canEditOffer = computed(() => (
currentOrder.value != null
&& ['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(currentOrder.value.status)
));
const offerSignature = computed(() => {
if (!currentOrder.value) {
return '';
}
return JSON.stringify({
deliveryTerms: deliveryTermsDraft.value.trim(),
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value),
itemPrices: currentOrder.value.items.map((item) => ({
itemId: item.id,
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? ''),
})),
});
});
const publishedSignature = computed(() => {
if (!currentOrder.value) {
return '';
}
return JSON.stringify({
deliveryTerms: currentOrder.value.deliveryTerms ?? '',
deliveryFee: currentOrder.value.deliveryFee ?? null,
itemPrices: currentOrder.value.items.map((item) => ({
itemId: item.id,
unitPrice: item.unitPrice ?? null,
})),
});
});
const offerReady = computed(() => {
if (!currentOrder.value) {
@@ -126,54 +156,28 @@ async function refetchOrder() {
}
async function saveOffer() {
if (!currentOrder.value || !offerReady.value) {
if (!currentOrder.value || !offerReady.value || !canEditOffer.value) {
return;
}
await setOfferMutation.mutate({
input: {
orderId: currentOrder.value.id,
itemPrices: currentOrder.value.items.map((item) => ({
itemId: item.id,
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0,
})),
deliveryTerms: deliveryTermsDraft.value.trim(),
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value) ?? 0,
},
});
await refetchOrder();
}
async function approveOrder() {
if (!currentOrder.value) {
return;
autosavePending.value = true;
try {
await setOfferMutation.mutate({
input: {
orderId: currentOrder.value.id,
itemPrices: currentOrder.value.items.map((item) => ({
itemId: item.id,
unitPrice: parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0,
})),
deliveryTerms: deliveryTermsDraft.value.trim(),
deliveryFee: parseMoneyDraft(deliveryFeeDraft.value) ?? 0,
},
});
await refetchOrder();
}
await finalizeMutation.mutate({ orderId: currentOrder.value.id, decision: 'APPROVE' });
await refetchOrder();
}
async function rejectOrder() {
if (!currentOrder.value) {
return;
finally {
autosavePending.value = false;
}
await finalizeMutation.mutate({ orderId: currentOrder.value.id, decision: 'REJECT' });
await refetchOrder();
}
async function blockOrder() {
if (!currentOrder.value) {
return;
}
await blockMutation.mutate({
input: {
orderId: currentOrder.value.id,
reason: 'Нужно уточнение параметров заказа.',
},
});
await refetchOrder();
}
async function startOrder() {
@@ -185,14 +189,23 @@ async function startOrder() {
await refetchOrder();
}
async function completeOrder() {
if (!currentOrder.value) {
return;
}
watch(
[offerReady, offerSignature, publishedSignature],
([ready, nextSignature, currentSignature]) => {
if (autosaveTimer) {
clearTimeout(autosaveTimer);
autosaveTimer = null;
}
await completeWorkMutation.mutate({ orderId: currentOrder.value.id });
await refetchOrder();
}
if (!ready || !canEditOffer.value || !nextSignature || nextSignature === currentSignature || autosavePending.value) {
return;
}
autosaveTimer = setTimeout(() => {
void saveOffer();
}, 700);
},
);
</script>
<template>
@@ -217,6 +230,7 @@ async function completeOrder() {
<OrdersOrderStatusTimelineCard
:status="currentOrder.status"
:created-at="currentOrder.createdAt"
audience="manager"
/>
<div class="surface-card rounded-3xl p-5">
@@ -237,6 +251,7 @@ async function completeOrder() {
step="0.01"
placeholder="Например, 125.50"
class="input input-bordered w-full rounded-2xl bg-white"
:disabled="!canEditOffer"
>
</label>
</li>
@@ -258,6 +273,7 @@ async function completeOrder() {
type="text"
placeholder="Например, доставка до склада 2-3 дня"
class="input input-bordered w-full rounded-2xl bg-white"
:disabled="!canEditOffer"
>
</label>
</div>
@@ -272,6 +288,7 @@ async function completeOrder() {
step="0.01"
placeholder="Например, 3000"
class="input input-bordered w-full rounded-2xl bg-white"
:disabled="!canEditOffer"
>
</label>
</div>
@@ -282,29 +299,21 @@ async function completeOrder() {
{{
offerTotal == null
? 'Итог по заказу посчитается автоматически после заполнения всех цен.'
: `Предварительный итог: ${formatPrice(offerTotal)}`
: autosavePending
? `Сохраняем и отправляем клиенту: ${formatPrice(offerTotal)}`
: `Предложение отправится клиенту автоматически: ${formatPrice(offerTotal)}`
}}
</p>
<button
class="btn btn-primary rounded-full border-0 px-5"
:disabled="!offerReady || setOfferMutation.loading.value"
@click="saveOffer"
>
Сохранить условия
</button>
</div>
</div>
<div class="surface-card rounded-3xl p-5">
<div v-if="['WAITING_DOUBLE_CONFIRM', 'CONFIRMED'].includes(currentOrder.status)" class="surface-card rounded-3xl p-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-xl font-bold text-[#123824]">Действия менеджера</h2>
<div class="flex flex-wrap gap-2">
<button class="btn btn-success btn-sm border-0" @click="approveOrder">Подтвердить</button>
<button class="btn btn-error btn-sm border-0" @click="rejectOrder">Отклонить</button>
<button class="btn btn-warning btn-sm border-0" @click="blockOrder">Заблокировать</button>
<button class="btn btn-accent btn-sm border-0" :disabled="currentOrder.status !== 'CONFIRMED'" @click="startOrder">В работу</button>
<button class="btn btn-neutral btn-sm border-0" :disabled="currentOrder.status !== 'IN_PROGRESS'" @click="completeOrder">Завершить</button>
<div>
<h2 class="text-xl font-bold text-[#123824]">Следующий шаг</h2>
<p class="mt-1 text-sm text-[#5c7b69]">Когда всё согласовано, менеджер просто переводит заказ в работу.</p>
</div>
<button class="btn btn-accent rounded-full border-0 px-5" @click="startOrder">Пустить в работу</button>
</div>
</div>
</div>