Files
web-frontend/app/pages/client-orders/[id].vue
2026-04-04 13:45:39 +07:00

314 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
BlockOrderDocument,
CompleteOrderDocument,
ManagerFinalizeOrderDocument,
ManagerSetOrderOfferDocument,
OrderDetailDocument,
StartOrderWorkDocument,
type OrderDetailQuery,
} from '~/composables/graphql/generated';
import {
formatPrice,
orderLogisticsStateText,
orderDeliveryStateText,
orderLineStateText,
} from '~/composables/useOrderDetailPresentation';
definePageMeta({
middleware: ['manager-only'],
});
const route = useRoute();
const orderId = computed(() => String(route.params.id || ''));
type ManagerOrderItem = NonNullable<OrderDetailQuery['order']>;
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 currentOrder = computed<ManagerOrderItem | null>(() =>
orderQuery.result.value?.order ?? null,
);
watch(
currentOrder,
(order) => {
for (const key of Object.keys(itemPriceDrafts)) {
delete itemPriceDrafts[key];
}
if (!order) {
deliveryTermsDraft.value = '';
deliveryFeeDraft.value = '';
return;
}
for (const item of order.items) {
itemPriceDrafts[item.id] = item.unitPrice == null ? '' : String(item.unitPrice);
}
deliveryTermsDraft.value = order.deliveryTerms ?? '';
deliveryFeeDraft.value = order.deliveryFee == null ? '' : String(order.deliveryFee);
},
{ immediate: true },
);
function parseMoneyDraft(value: string) {
const trimmed = String(value).replace(',', '.').trim();
if (!trimmed) {
return null;
}
const normalized = Number(trimmed);
if (!Number.isFinite(normalized) || normalized < 0) {
return null;
}
return Math.round((normalized + Number.EPSILON) * 100) / 100;
}
function draftUnitPrice(itemId: string, fallback?: number | null) {
return parseMoneyDraft(itemPriceDrafts[itemId] ?? '') ?? fallback ?? null;
}
function draftLineTotal(item: ManagerOrderItem['items'][number]) {
const unitPrice = draftUnitPrice(item.id, item.unitPrice);
if (unitPrice == null) {
return null;
}
return Math.round((unitPrice * item.quantity + Number.EPSILON) * 100) / 100;
}
const draftDeliveryTerms = computed(() => deliveryTermsDraft.value.trim() || currentOrder.value?.deliveryTerms || null);
const draftDeliveryFee = computed(() => parseMoneyDraft(deliveryFeeDraft.value) ?? currentOrder.value?.deliveryFee ?? null);
const offerReady = computed(() => {
if (!currentOrder.value) {
return false;
}
const deliveryFee = parseMoneyDraft(deliveryFeeDraft.value);
if (deliveryFee == null) {
return false;
}
return currentOrder.value.items.every((item) => parseMoneyDraft(itemPriceDrafts[item.id] ?? '') != null);
});
const offerTotal = computed(() => {
if (!currentOrder.value || !offerReady.value) {
return null;
}
const productsTotal = currentOrder.value.items.reduce((sum, item) => (
sum + item.quantity * (parseMoneyDraft(itemPriceDrafts[item.id] ?? '') ?? 0)
), 0);
return Math.round((productsTotal + (parseMoneyDraft(deliveryFeeDraft.value) ?? 0) + Number.EPSILON) * 100) / 100;
});
async function refetchOrder() {
await orderQuery.refetch({ id: orderId.value });
}
async function saveOffer() {
if (!currentOrder.value || !offerReady.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;
}
await finalizeMutation.mutate({ orderId: currentOrder.value.id, decision: 'APPROVE' });
await refetchOrder();
}
async function rejectOrder() {
if (!currentOrder.value) {
return;
}
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() {
if (!currentOrder.value) {
return;
}
await startWorkMutation.mutate({ orderId: currentOrder.value.id });
await refetchOrder();
}
async function completeOrder() {
if (!currentOrder.value) {
return;
}
await completeWorkMutation.mutate({ orderId: currentOrder.value.id });
await refetchOrder();
}
</script>
<template>
<section class="space-y-6">
<NuxtLink to="/client-orders" class="text-sm font-semibold text-[#0d854a]"> Назад к заказам клиентов</NuxtLink>
<div v-if="orderQuery.loading.value" class="manager-empty-state">
Загружаем заказ...
</div>
<div v-else-if="!currentOrder" class="manager-empty-state">
Заказ не найден.
</div>
<template v-else>
<div class="manager-hero">
<p class="manager-eyebrow">Заказ</p>
<h1 class="manager-title">{{ currentOrder.code }}</h1>
</div>
<div class="space-y-4">
<OrdersOrderStatusTimelineCard
:status="currentOrder.status"
:created-at="currentOrder.createdAt"
/>
<div class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Состав заказа</h2>
<ul class="mt-4 space-y-3">
<li v-for="item in currentOrder.items" :key="item.id" class="manager-mini-card space-y-3">
<div class="space-y-1">
<p class="text-sm font-semibold text-[#123824]">{{ item.productName }} × {{ item.quantity }}</p>
<p class="text-sm text-[#5c7b69]">{{ orderLineStateText(draftUnitPrice(item.id, item.unitPrice), draftLineTotal(item)) }}</p>
</div>
<label class="form-control">
<span class="mb-2 text-xs font-semibold uppercase tracking-[0.22em] text-[#6a8a76]">Цена за единицу</span>
<input
v-model="itemPriceDrafts[item.id]"
type="number"
min="0"
step="0.01"
placeholder="Например, 125.50"
class="input input-bordered w-full rounded-2xl bg-white"
>
</label>
</li>
</ul>
</div>
<div class="surface-card rounded-3xl p-5">
<h2 class="text-xl font-bold text-[#123824]">Доставка</h2>
<div class="mt-4 space-y-3">
<div class="manager-mini-card text-sm text-[#123824]">
Адрес: {{ currentOrder.deliveryAddress || 'Адрес пока не указан' }}
</div>
<div class="manager-mini-card space-y-3 text-sm text-[#123824]">
<p>{{ orderDeliveryStateText(draftDeliveryTerms) }}</p>
<label class="form-control">
<span class="mb-2 text-xs font-semibold uppercase tracking-[0.22em] text-[#6a8a76]">Комментарий по доставке</span>
<input
v-model="deliveryTermsDraft"
type="text"
placeholder="Например, доставка до склада 2-3 дня"
class="input input-bordered w-full rounded-2xl bg-white"
>
</label>
</div>
<div class="manager-mini-card space-y-3 text-sm text-[#123824]">
<p>{{ orderLogisticsStateText(draftDeliveryFee) }}</p>
<label class="form-control">
<span class="mb-2 text-xs font-semibold uppercase tracking-[0.22em] text-[#6a8a76]">Стоимость логистики</span>
<input
v-model="deliveryFeeDraft"
type="number"
min="0"
step="0.01"
placeholder="Например, 3000"
class="input input-bordered w-full rounded-2xl bg-white"
>
</label>
</div>
</div>
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-[#d6ebde] pt-4">
<p class="text-sm text-[#5c7b69]">
{{
offerTotal == null
? 'Итог по заказу посчитается автоматически после заполнения всех цен.'
: `Предварительный итог: ${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 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>
</div>
</div>
</div>
</template>
</section>
</template>