Files
web-frontend/app/pages/orders/index.vue
2026-04-07 10:25:28 +07:00

174 lines
6.2 KiB
Vue
Raw Permalink 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 {
MyOrdersDocument,
type MyOrdersQuery,
} from '~/composables/graphql/generated';
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
type OrderItem = MyOrdersQuery['myOrders'][number];
const allOrders = useQuery(MyOrdersDocument);
const search = ref('');
const statusFilter = ref<'ALL' | 'WAITING' | 'ACTIVE' | 'CLOSED'>('ALL');
const dateFrom = ref('');
const dateTo = ref('');
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']);
function matchesFilter(order: OrderItem) {
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 matchesDate(order: OrderItem) {
const orderTimestamp = new Date(order.createdAt).getTime();
if (!Number.isFinite(orderTimestamp)) {
return false;
}
if (dateFrom.value) {
const fromTimestamp = new Date(`${dateFrom.value}T00:00:00`).getTime();
if (Number.isFinite(fromTimestamp) && orderTimestamp < fromTimestamp) {
return false;
}
}
if (dateTo.value) {
const toTimestamp = new Date(`${dateTo.value}T23:59:59.999`).getTime();
if (Number.isFinite(toTimestamp) && orderTimestamp > toTimestamp) {
return false;
}
}
return true;
}
const filteredOrders = computed(() => {
const orders = allOrders.result.value?.myOrders ?? [];
const normalizedSearch = search.value.trim().toLowerCase();
return orders.filter((order) => {
const text = [
order.code,
formatOrderCode(order.code),
...order.items.map((item) => item.productName),
]
.join(' ')
.toLowerCase();
const matchSearch = !normalizedSearch || text.includes(normalizedSearch);
return matchSearch && matchesFilter(order) && matchesDate(order);
});
});
const {
canLoadMore,
loadMore,
loadMoreSentinel,
remainingCount,
visibleItems: visibleOrders,
} = useIncrementalList(filteredOrders, {
pageSize: 24,
resetKeys: [search, statusFilter, dateFrom, dateTo],
});
</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 md:flex-wrap md:justify-end">
<label class="flex items-center gap-2 rounded-full border border-[#d7e9de] bg-white px-4 py-3 text-sm font-semibold text-[#123824]">
<svg class="h-4 w-4 text-[#6b8576]" viewBox="0 0 20 20" fill="none">
<path d="M3.33334 5H16.6667" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
<path d="M6.66666 10H13.3333" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
<path d="M8.33334 15H11.6667" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
</svg>
<select
v-model="statusFilter"
class="min-w-0 bg-transparent outline-none"
>
<option value="ALL">Все заказы</option>
<option value="WAITING">Ожидают подтверждения</option>
<option value="ACTIVE">Активные</option>
<option value="CLOSED">Закрытые</option>
</select>
</label>
<label class="flex items-center gap-2 rounded-full border border-[#d7e9de] bg-white px-4 py-3 text-sm font-semibold text-[#123824]">
<svg class="h-4 w-4 text-[#6b8576]" viewBox="0 0 20 20" fill="none">
<rect x="3" y="4.5" width="14" height="12" rx="2.5" stroke="currentColor" stroke-width="1.6" />
<path d="M6 2.5V6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
<path d="M14 2.5V6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
<path d="M3 8.5H17" stroke="currentColor" stroke-width="1.6" />
</svg>
<input
v-model="dateFrom"
type="date"
class="min-w-0 bg-transparent outline-none"
>
</label>
<label class="flex items-center gap-2 rounded-full border border-[#d7e9de] bg-white px-4 py-3 text-sm font-semibold text-[#123824]">
<svg class="h-4 w-4 text-[#6b8576]" viewBox="0 0 20 20" fill="none">
<rect x="3" y="4.5" width="14" height="12" rx="2.5" stroke="currentColor" stroke-width="1.6" />
<path d="M6 2.5V6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
<path d="M14 2.5V6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
<path d="M3 8.5H17" stroke="currentColor" stroke-width="1.6" />
</svg>
<input
v-model="dateTo"
type="date"
class="min-w-0 bg-transparent outline-none"
>
</label>
</div>
</template>
</UiSectionSearchHero>
<div v-if="allOrders.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-3">
<OrdersOrderSummaryCard
v-for="order in visibleOrders"
:key="order.id"
:to="`/orders/${order.id}`"
:code="order.code"
:status="order.status"
:created-at="order.createdAt"
:total-price="order.totalPrice"
:items="order.items"
/>
<div
v-if="canLoadMore"
ref="loadMoreSentinel"
class="flex justify-center pt-2"
>
<button class="btn btn-outline border-[#d7e9de] bg-white" @click="loadMore">
Показать ещё {{ Math.min(remainingCount, 24) }}
</button>
</div>
</div>
</section>
</template>