Files
web-frontend/app/pages/client-orders/index.vue
2026-04-04 09:29:16 +07:00

169 lines
5.4 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 { useQuery } from '@vue/apollo-composable';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import ruLocale from '@fullcalendar/core/locales/ru';
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
import {
ManagerOrdersDocument,
type ManagerOrdersQuery,
} from '~/composables/graphql/generated';
definePageMeta({
middleware: ['manager-only'],
});
type ManagerOrderItem = ManagerOrdersQuery['managerOrders'][number];
const route = useRoute();
const router = useRouter();
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 search = ref('');
const statusFilter = ref<'ALL' | 'WAITING' | 'ACTIVE' | 'CLOSED'>('ALL');
const viewMode = computed<'cards' | 'calendar'>(() => (
route.query.view === 'calendar' ? 'calendar' : 'cards'
));
function setViewMode(view: 'cards' | 'calendar') {
void router.replace({
query: {
...route.query,
view,
},
});
}
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);
}
const filteredOrders = computed(() => {
const orders = ordersQuery.result.value?.managerOrders ?? [];
const query = search.value.trim().toLowerCase();
return orders.filter((order) => {
const text = [
order.code,
order.customerId,
order.deliveryAddress || '',
...order.items.map((item) => item.productName),
]
.join(' ')
.toLowerCase();
const matchesSearch = !query || text.includes(query);
return matchesSearch && matchesFilter(order);
});
});
const calendarOptions = computed(() => ({
plugins: [dayGridPlugin],
locale: ruLocale,
initialView: 'dayGridMonth',
height: 'auto',
fixedWeekCount: false,
firstDay: 1,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: '',
},
buttonText: {
today: 'Сегодня',
},
events: filteredOrders.value.map((order) => ({
id: order.id,
title: `${order.code}${order.customerId}`,
start: new Date(order.createdAt).toISOString(),
allDay: true,
})),
eventClick: ({ event }: { event: { id: string } }) => {
void router.push(`/client-orders/${event.id}`);
},
}));
</script>
<template>
<section class="space-y-6">
<UiSectionSearchHero
v-model="search"
title="Заказы"
search-placeholder="Номер заказа, клиент, адрес или товар"
>
<template #controls>
<div class="flex w-full flex-col gap-3 md:w-auto md:flex-row">
<select v-model="statusFilter" class="select select-bordered w-full rounded-full bg-white md:w-64">
<option value="ALL">Все заказы</option>
<option value="WAITING">Ожидают подтверждения</option>
<option value="ACTIVE">Активные</option>
<option value="CLOSED">Закрытые</option>
</select>
<div class="tabs tabs-boxed w-fit bg-white">
<button class="tab" :class="{ 'tab-active': viewMode === 'cards' }" @click="setViewMode('cards')">
Карточки
</button>
<button class="tab" :class="{ 'tab-active': viewMode === 'calendar' }" @click="setViewMode('calendar')">
Календарь
</button>
</div>
</div>
</template>
</UiSectionSearchHero>
<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-if="viewMode === 'calendar'" class="surface-card rounded-3xl p-4 md:p-5">
<FullCalendar :options="calendarOptions" />
</div>
<div v-else class="space-y-4">
<NuxtLink
v-for="order in filteredOrders"
:key="order.id"
:to="`/client-orders/${order.id}`"
class="surface-card block rounded-3xl p-5"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="space-y-1">
<h2 class="text-lg font-bold text-[#123824]">{{ order.code }}</h2>
<p class="text-sm text-[#5c7b69]">Клиент: {{ order.customerId }}</p>
<p class="text-sm text-[#5c7b69]">Создан: {{ new Date(order.createdAt).toLocaleString() }}</p>
<p v-if="order.deliveryAddress" class="text-sm text-[#5c7b69]">Адрес: {{ order.deliveryAddress }}</p>
</div>
<OrderStatusBadge :status="order.status" />
</div>
<ul class="mt-4 grid gap-2 text-sm text-[#214735]">
<li
v-for="item in order.items"
:key="item.id"
class="rounded-2xl border border-[#d6ebde] bg-white px-4 py-3"
>
{{ item.productName }} × {{ item.quantity }}
</li>
</ul>
</NuxtLink>
</div>
</section>
</template>