Refine order code and calendar cards

This commit is contained in:
Ruslan Bakiev
2026-04-04 15:27:00 +07:00
parent 309d0e78db
commit e20565b4ae
6 changed files with 167 additions and 9 deletions

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue'; import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
import { formatPrice } from '~/composables/useOrderDetailPresentation'; import { formatPrice } from '~/composables/useOrderDetailPresentation';
type OrderCardItem = { type OrderCardItem = {
@@ -63,6 +64,7 @@ function createProductCover(name: string, seedKey: string) {
const visibleItems = computed(() => props.items.slice(0, 4)); const visibleItems = computed(() => props.items.slice(0, 4));
const hiddenCount = computed(() => Math.max(props.items.length - visibleItems.value.length, 0)); const hiddenCount = computed(() => Math.max(props.items.length - visibleItems.value.length, 0));
const totalPriceLabel = computed(() => formatPrice(props.totalPrice) ?? '—'); const totalPriceLabel = computed(() => formatPrice(props.totalPrice) ?? '—');
const codeLabel = computed(() => formatOrderCode(props.code));
</script> </script>
<template> <template>
@@ -72,7 +74,7 @@ const totalPriceLabel = computed(() => formatPrice(props.totalPrice) ?? '—');
> >
<div class="grid gap-4 md:grid-cols-4 md:items-center md:gap-6"> <div class="grid gap-4 md:grid-cols-4 md:items-center md:gap-6">
<div class="min-w-0"> <div class="min-w-0">
<h2 class="truncate text-lg font-bold text-[#123824]">{{ code }}</h2> <h2 class="truncate text-lg font-bold text-[#123824]">{{ codeLabel }}</h2>
<p class="mt-1 text-sm text-[#688676]">{{ formatCreatedAt(createdAt) }}</p> <p class="mt-1 text-sm text-[#688676]">{{ formatCreatedAt(createdAt) }}</p>
</div> </div>

View File

@@ -0,0 +1,17 @@
export function extractOrderCodeShort(code?: string | null) {
const normalized = String(code ?? '').trim();
if (!normalized) {
return '0000';
}
const digits = normalized.replace(/\D/g, '');
if (digits.length >= 4) {
return digits.slice(-4);
}
return normalized.slice(-4).toUpperCase().padStart(4, '0');
}
export function formatOrderCode(code?: string | null) {
return `#${extractOrderCodeShort(code)}`;
}

View File

@@ -11,6 +11,7 @@ import {
orderLogisticsStateText, orderLogisticsStateText,
orderDeliveryStateText, orderDeliveryStateText,
} from '~/composables/useOrderDetailPresentation'; } from '~/composables/useOrderDetailPresentation';
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
definePageMeta({ definePageMeta({
middleware: ['manager-only'], middleware: ['manager-only'],
@@ -38,6 +39,8 @@ const currentOrder = computed<ManagerOrderItem | null>(() =>
orderQuery.result.value?.order ?? null, orderQuery.result.value?.order ?? null,
); );
const currentOrderCode = computed(() => formatOrderCode(currentOrder.value?.code));
watch( watch(
currentOrder, currentOrder,
(order) => { (order) => {
@@ -209,7 +212,7 @@ watch(
<template v-else> <template v-else>
<div class="manager-hero"> <div class="manager-hero">
<p class="manager-eyebrow">Заказ</p> <p class="manager-eyebrow">Заказ</p>
<h1 class="manager-title">{{ currentOrder.code }}</h1> <h1 class="manager-title">{{ currentOrderCode }}</h1>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">

View File

@@ -10,6 +10,8 @@ import {
type ManagerUsersQuery, type ManagerUsersQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
import { messengerConnectionAvatarSrc } from '~/composables/useMessengerConnectionPresentation'; import { messengerConnectionAvatarSrc } from '~/composables/useMessengerConnectionPresentation';
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
import { formatPrice } from '~/composables/useOrderDetailPresentation';
definePageMeta({ definePageMeta({
middleware: ['manager-only'], middleware: ['manager-only'],
@@ -52,6 +54,7 @@ function customerCardMeta(customerId: string) {
return { return {
name: customerId, name: customerId,
avatarSrc: '', avatarSrc: '',
fallbackAvatarSrc: '/favicon.ico',
initials: customerId.slice(0, 2).toUpperCase(), initials: customerId.slice(0, 2).toUpperCase(),
}; };
} }
@@ -59,6 +62,7 @@ function customerCardMeta(customerId: string) {
return { return {
name: customer.fullName, name: customer.fullName,
avatarSrc: messengerConnectionAvatarSrc(customer.telegramConnection), avatarSrc: messengerConnectionAvatarSrc(customer.telegramConnection),
fallbackAvatarSrc: '/favicon.ico',
initials: customer.fullName initials: customer.fullName
.split(/\s+/) .split(/\s+/)
.filter(Boolean) .filter(Boolean)
@@ -95,6 +99,7 @@ const searchedOrders = computed<ManagerOrderItem[]>(() => {
return orders.filter((order) => { return orders.filter((order) => {
const text = [ const text = [
order.code, order.code,
formatOrderCode(order.code),
order.customerId, order.customerId,
customerCardMeta(order.customerId).name, customerCardMeta(order.customerId).name,
order.deliveryAddress || '', order.deliveryAddress || '',
@@ -109,6 +114,15 @@ const searchedOrders = computed<ManagerOrderItem[]>(() => {
const filteredOrders = computed<ManagerOrderItem[]>(() => searchedOrders.value.filter((order) => matchesFilter(order))); const filteredOrders = computed<ManagerOrderItem[]>(() => searchedOrders.value.filter((order) => matchesFilter(order)));
function escapeHtml(value: string) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
const statusTabs = computed<Array<{ id: StatusFilter; label: string; count: number }>>(() => [ const statusTabs = computed<Array<{ id: StatusFilter; label: string; count: number }>>(() => [
{ {
id: 'ALL', id: 'ALL',
@@ -152,12 +166,50 @@ const calendarOptions = computed(() => ({
buttonText: { buttonText: {
today: 'Сегодня', today: 'Сегодня',
}, },
events: filteredOrders.value.map((order: ManagerOrderItem) => ({ events: filteredOrders.value.map((order: ManagerOrderItem) => {
const customer = customerCardMeta(order.customerId);
return {
id: order.id, id: order.id,
title: `${order.code}${order.customerId}`, title: formatOrderCode(order.code),
start: new Date(order.createdAt).toISOString(), start: new Date(order.createdAt).toISOString(),
allDay: true, allDay: true,
})), extendedProps: {
customerName: customer.name,
avatarSrc: customer.avatarSrc,
fallbackAvatarSrc: customer.fallbackAvatarSrc,
initials: customer.initials,
orderCode: formatOrderCode(order.code),
totalPriceLabel: formatPrice(order.totalPrice) ?? 'Цена уточняется',
},
};
}),
eventContent: ({ event }: { event: { extendedProps: Record<string, string> } }) => {
const customerName = escapeHtml(event.extendedProps.customerName || '');
const avatarSrc = event.extendedProps.avatarSrc || event.extendedProps.fallbackAvatarSrc;
const orderCode = escapeHtml(event.extendedProps.orderCode || '');
const totalPriceLabel = escapeHtml(event.extendedProps.totalPriceLabel || '');
const initials = escapeHtml(event.extendedProps.initials || '');
return {
html: `
<div class="manager-calendar-order-card">
<div class="manager-calendar-order-card__header">
<div class="manager-calendar-order-card__avatar-shell">
${avatarSrc
? `<img src="${escapeHtml(avatarSrc)}" alt="${customerName}" class="manager-calendar-order-card__avatar">`
: `<span class="manager-calendar-order-card__initials">${initials}</span>`}
</div>
<div class="manager-calendar-order-card__text">
<div class="manager-calendar-order-card__code">${orderCode}</div>
<div class="manager-calendar-order-card__name">${customerName}</div>
</div>
</div>
<div class="manager-calendar-order-card__price">${totalPriceLabel}</div>
</div>
`,
};
},
eventClick: ({ event }: { event: { id: string } }) => { eventClick: ({ event }: { event: { id: string } }) => {
void router.push(`/client-orders/${event.id}`); void router.push(`/client-orders/${event.id}`);
}, },
@@ -238,3 +290,82 @@ const calendarOptions = computed(() => ({
</div> </div>
</section> </section>
</template> </template>
<style>
.manager-calendar-order-card {
display: grid;
gap: 0.45rem;
min-height: 74px;
border-radius: 18px;
border: 1px solid #dcebe3;
background: linear-gradient(180deg, #ffffff 0%, #f4faf6 100%);
padding: 0.62rem 0.72rem;
box-shadow: 0 10px 24px rgba(18, 56, 36, 0.08);
}
.manager-calendar-order-card__header {
display: flex;
align-items: center;
gap: 0.55rem;
min-width: 0;
}
.manager-calendar-order-card__avatar-shell {
width: 34px;
height: 34px;
flex: 0 0 auto;
overflow: hidden;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e4f6eb 0%, #c6e7d5 100%);
}
.manager-calendar-order-card__avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.manager-calendar-order-card__initials {
font-size: 0.78rem;
font-weight: 800;
color: #123824;
}
.manager-calendar-order-card__text {
min-width: 0;
}
.manager-calendar-order-card__code {
font-size: 0.88rem;
line-height: 1.1;
font-weight: 800;
color: #123824;
}
.manager-calendar-order-card__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.72rem;
color: #5a7968;
}
.manager-calendar-order-card__price {
font-size: 0.78rem;
font-weight: 700;
color: #0d854a;
}
.fc .fc-daygrid-event {
border: 0;
background: transparent;
}
.fc .fc-daygrid-dot-event:hover,
.fc .fc-daygrid-event:hover {
background: transparent;
}
</style>

View File

@@ -4,6 +4,7 @@ import {
OrderDetailDocument, OrderDetailDocument,
type OrderDetailQuery, type OrderDetailQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
import { import {
orderDeliveryStateText, orderDeliveryStateText,
orderLogisticsStateText, orderLogisticsStateText,
@@ -20,6 +21,8 @@ const orderQuery = useQuery(OrderDetailDocument, () => ({
const currentOrder = computed<OrderItem | null>(() => const currentOrder = computed<OrderItem | null>(() =>
orderQuery.result.value?.order ?? null, orderQuery.result.value?.order ?? null,
); );
const currentOrderCode = computed(() => formatOrderCode(currentOrder.value?.code));
</script> </script>
<template> <template>
@@ -37,7 +40,7 @@ const currentOrder = computed<OrderItem | null>(() =>
<template v-else> <template v-else>
<div class="manager-hero"> <div class="manager-hero">
<p class="manager-eyebrow">Заказ</p> <p class="manager-eyebrow">Заказ</p>
<h1 class="manager-title">{{ currentOrder.code }}</h1> <h1 class="manager-title">{{ currentOrderCode }}</h1>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">

View File

@@ -4,6 +4,7 @@ import {
MyOrdersDocument, MyOrdersDocument,
type MyOrdersQuery, type MyOrdersQuery,
} from '~/composables/graphql/generated'; } from '~/composables/graphql/generated';
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
type OrderItem = MyOrdersQuery['myOrders'][number]; type OrderItem = MyOrdersQuery['myOrders'][number];
@@ -34,6 +35,7 @@ const filteredOrders = computed(() => {
return orders.filter((order) => { return orders.filter((order) => {
const text = [ const text = [
order.code, order.code,
formatOrderCode(order.code),
...order.items.map((item) => item.productName), ...order.items.map((item) => item.productName),
] ]
.join(' ') .join(' ')