Files
web-frontend/app/pages/client-orders.vue
2026-04-03 19:01:22 +07:00

290 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 OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
import {
BlockOrderDocument,
CompleteOrderDocument,
ManagerFinalizeOrderDocument,
ManagerOrdersDocument,
ManagerSetOrderOfferDocument,
StartOrderWorkDocument,
type ManagerOrdersQuery,
} from '~/composables/graphql/generated';
definePageMeta({
middleware: ['manager-only'],
});
type ManagerOrderItem = ManagerOrdersQuery['managerOrders'][number];
const ACTIVE_STATUSES = new Set(['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'IN_PROGRESS']);
const CLOSED_STATUSES = new Set(['COMPLETED', 'CLIENT_REJECTED', 'MANAGER_REJECTED', 'MANAGER_BLOCKED']);
const ordersQuery = useQuery(ManagerOrdersDocument, { status: null });
const setOfferMutation = useMutation(ManagerSetOrderOfferDocument);
const finalizeMutation = useMutation(ManagerFinalizeOrderDocument);
const blockMutation = useMutation(BlockOrderDocument);
const startWorkMutation = useMutation(StartOrderWorkDocument);
const completeWorkMutation = useMutation(CompleteOrderDocument);
const search = ref('');
const statusFilter = ref<'ALL' | 'WAITING' | 'ACTIVE' | 'CLOSED'>('ALL');
const offerForm = reactive<Record<string, { deliveryTerms: string; deliveryFee: number; totalPrice: number }>>({});
watchEffect(() => {
for (const order of ordersQuery.result.value?.managerOrders ?? []) {
if (!offerForm[order.id]) {
offerForm[order.id] = {
deliveryTerms: order.deliveryTerms || 'Доставка 3-5 дней',
deliveryFee: Number(order.deliveryFee ?? 1000),
totalPrice: Number(order.totalPrice ?? 12500),
};
}
}
});
function matchesFilter(order: ManagerOrderItem) {
if (statusFilter.value === 'ALL') {
return true;
}
if (statusFilter.value === 'WAITING') {
return order.status === 'WAITING_DOUBLE_CONFIRM';
}
if (statusFilter.value === 'ACTIVE') {
return ACTIVE_STATUSES.has(order.status);
}
return CLOSED_STATUSES.has(order.status);
}
function getOffer(orderId: string) {
if (!offerForm[orderId]) {
offerForm[orderId] = {
deliveryTerms: 'Доставка 3-5 дней',
deliveryFee: 1000,
totalPrice: 12500,
};
}
return offerForm[orderId];
}
function updateOfferField(orderId: string, field: 'deliveryTerms' | 'deliveryFee' | 'totalPrice', value: string) {
const offer = getOffer(orderId);
if (field === 'deliveryTerms') {
offer.deliveryTerms = value;
return;
}
const numericValue = Number(value);
offer[field] = Number.isFinite(numericValue) ? numericValue : 0;
}
const filteredOrders = computed(() => {
const orders = ordersQuery.result.value?.managerOrders ?? [];
const query = search.value.trim().toLowerCase();
return orders.filter((order) => {
const haystack = [
order.code,
order.customerId || '',
order.deliveryAddress || '',
...order.items.map((item) => item.productName),
]
.join(' ')
.toLowerCase();
const matchSearch = !query || haystack.includes(query);
return matchSearch && matchesFilter(order);
});
});
async function refetchOrders() {
await ordersQuery.refetch({
status: null,
});
}
async function publishOffer(orderId: string) {
const form = getOffer(orderId);
await setOfferMutation.mutate({
input: {
orderId,
deliveryTerms: form.deliveryTerms,
deliveryFee: Number(form.deliveryFee),
totalPrice: Number(form.totalPrice),
},
});
await refetchOrders();
}
async function approve(orderId: string) {
await finalizeMutation.mutate({
orderId,
decision: 'APPROVE',
});
await refetchOrders();
}
async function reject(orderId: string) {
await finalizeMutation.mutate({
orderId,
decision: 'REJECT',
});
await refetchOrders();
}
async function blockOrder(orderId: string) {
await blockMutation.mutate({
input: {
orderId,
reason: 'Нужно уточнение параметров заказа.',
},
});
await refetchOrders();
}
async function start(orderId: string) {
await startWorkMutation.mutate({
orderId,
});
await refetchOrders();
}
async function complete(orderId: string) {
await completeWorkMutation.mutate({
orderId,
});
await refetchOrders();
}
</script>
<template>
<section class="space-y-6">
<div class="manager-hero">
<p class="manager-eyebrow">Заказы клиентов</p>
<h1 class="manager-title">Разбор заявок, офферов и статусов без отдельной менеджерки</h1>
<p class="manager-copy">
Фильтруйте заявки, публикуйте условия доставки, подтверждайте работу и доводите заказ до завершения прямо в основном кабинете.
</p>
</div>
<div class="surface-card rounded-3xl p-4 md:p-5">
<div class="grid gap-3 md:grid-cols-[1fr_auto]">
<label class="form-control">
<span class="label-text">Поиск</span>
<input
v-model="search"
type="text"
class="input manager-field w-full"
placeholder="Номер, пользователь, адрес или товар"
>
</label>
<label class="form-control md:min-w-60">
<span class="label-text">Фильтр</span>
<select v-model="statusFilter" class="select manager-field w-full">
<option value="ALL">Все заказы</option>
<option value="WAITING">Ожидают подтверждения</option>
<option value="ACTIVE">Активные</option>
<option value="CLOSED">Закрытые</option>
</select>
</label>
</div>
</div>
<div v-if="ordersQuery.loading.value" class="manager-empty-state">
Загружаем очередь заказов...
</div>
<div v-else-if="filteredOrders.length === 0" class="manager-empty-state">
По текущим условиям заказов не найдено.
</div>
<div v-else class="space-y-4">
<article
v-for="order in filteredOrders"
:key="order.id"
class="surface-card rounded-3xl p-5"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="space-y-1">
<h2 class="text-xl font-bold text-[#123824]">{{ order.code }}</h2>
<div class="flex flex-wrap gap-3 text-sm text-[#5c7b69]">
<span>Клиент: {{ order.customerId || 'не указан' }}</span>
<span>{{ new Date(order.createdAt).toLocaleString() }}</span>
</div>
</div>
<OrderStatusBadge :status="order.status" />
</div>
<div class="mt-4 grid gap-3 lg:grid-cols-[1.1fr_0.9fr]">
<div class="space-y-3">
<div class="surface-subcard p-4">
<p class="text-sm font-semibold text-[#123824]">Состав заказа</p>
<ul class="mt-3 space-y-2 text-sm text-[#214735]">
<li
v-for="item in order.items"
:key="item.id"
class="manager-mini-card"
>
{{ item.productName }} × {{ item.quantity }}
</li>
</ul>
</div>
<div class="surface-subcard p-4">
<p class="text-sm font-semibold text-[#123824]">Доставка</p>
<div class="mt-3 grid gap-3 text-sm text-[#214735] md:grid-cols-2">
<div class="manager-mini-card">
Адрес: {{ order.deliveryAddress || 'клиент еще не выбрал адрес' }}
</div>
<div class="manager-mini-card">
Условия: {{ order.deliveryTerms || 'ожидает оффера менеджера' }}
</div>
</div>
</div>
</div>
<div class="surface-subcard p-4">
<p class="text-sm font-semibold text-[#123824]">Оффер и действия</p>
<div class="mt-3 space-y-3">
<input
:value="getOffer(order.id).deliveryTerms"
class="input manager-field w-full"
placeholder="Условия доставки"
@input="updateOfferField(order.id, 'deliveryTerms', ($event.target as HTMLInputElement).value)"
>
<input
:value="getOffer(order.id).deliveryFee"
type="number"
class="input manager-field w-full"
placeholder="Стоимость доставки"
@input="updateOfferField(order.id, 'deliveryFee', ($event.target as HTMLInputElement).value)"
>
<input
:value="getOffer(order.id).totalPrice"
type="number"
class="input manager-field w-full"
placeholder="Итоговая стоимость"
@input="updateOfferField(order.id, 'totalPrice', ($event.target as HTMLInputElement).value)"
>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button class="btn btn-primary btn-sm border-0" @click="publishOffer(order.id)">Публиковать оффер</button>
<button class="btn btn-success btn-sm border-0" @click="approve(order.id)">Подтвердить</button>
<button class="btn btn-error btn-sm border-0" @click="reject(order.id)">Отклонить</button>
<button class="btn btn-warning btn-sm border-0" @click="blockOrder(order.id)">Заблокировать</button>
<button class="btn btn-accent btn-sm border-0" :disabled="order.status !== 'CONFIRMED'" @click="start(order.id)">В работу</button>
<button class="btn btn-neutral btn-sm border-0" :disabled="order.status !== 'IN_PROGRESS'" @click="complete(order.id)">Завершить</button>
</div>
</div>
</div>
</article>
</div>
</section>
</template>