Refine order code and calendar cards
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
||||
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
|
||||
import { formatPrice } from '~/composables/useOrderDetailPresentation';
|
||||
|
||||
type OrderCardItem = {
|
||||
@@ -63,6 +64,7 @@ function createProductCover(name: string, seedKey: string) {
|
||||
const visibleItems = computed(() => props.items.slice(0, 4));
|
||||
const hiddenCount = computed(() => Math.max(props.items.length - visibleItems.value.length, 0));
|
||||
const totalPriceLabel = computed(() => formatPrice(props.totalPrice) ?? '—');
|
||||
const codeLabel = computed(() => formatOrderCode(props.code));
|
||||
</script>
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
|
||||
17
app/composables/useOrderCodePresentation.ts
Normal file
17
app/composables/useOrderCodePresentation.ts
Normal 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)}`;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
orderLogisticsStateText,
|
||||
orderDeliveryStateText,
|
||||
} from '~/composables/useOrderDetailPresentation';
|
||||
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['manager-only'],
|
||||
@@ -38,6 +39,8 @@ const currentOrder = computed<ManagerOrderItem | null>(() =>
|
||||
orderQuery.result.value?.order ?? null,
|
||||
);
|
||||
|
||||
const currentOrderCode = computed(() => formatOrderCode(currentOrder.value?.code));
|
||||
|
||||
watch(
|
||||
currentOrder,
|
||||
(order) => {
|
||||
@@ -209,7 +212,7 @@ watch(
|
||||
<template v-else>
|
||||
<div class="manager-hero">
|
||||
<p class="manager-eyebrow">Заказ</p>
|
||||
<h1 class="manager-title">{{ currentOrder.code }}</h1>
|
||||
<h1 class="manager-title">{{ currentOrderCode }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
type ManagerUsersQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import { messengerConnectionAvatarSrc } from '~/composables/useMessengerConnectionPresentation';
|
||||
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
|
||||
import { formatPrice } from '~/composables/useOrderDetailPresentation';
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['manager-only'],
|
||||
@@ -52,6 +54,7 @@ function customerCardMeta(customerId: string) {
|
||||
return {
|
||||
name: customerId,
|
||||
avatarSrc: '',
|
||||
fallbackAvatarSrc: '/favicon.ico',
|
||||
initials: customerId.slice(0, 2).toUpperCase(),
|
||||
};
|
||||
}
|
||||
@@ -59,6 +62,7 @@ function customerCardMeta(customerId: string) {
|
||||
return {
|
||||
name: customer.fullName,
|
||||
avatarSrc: messengerConnectionAvatarSrc(customer.telegramConnection),
|
||||
fallbackAvatarSrc: '/favicon.ico',
|
||||
initials: customer.fullName
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
@@ -95,6 +99,7 @@ const searchedOrders = computed<ManagerOrderItem[]>(() => {
|
||||
return orders.filter((order) => {
|
||||
const text = [
|
||||
order.code,
|
||||
formatOrderCode(order.code),
|
||||
order.customerId,
|
||||
customerCardMeta(order.customerId).name,
|
||||
order.deliveryAddress || '',
|
||||
@@ -109,6 +114,15 @@ const searchedOrders = computed<ManagerOrderItem[]>(() => {
|
||||
|
||||
const filteredOrders = computed<ManagerOrderItem[]>(() => searchedOrders.value.filter((order) => matchesFilter(order)));
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
const statusTabs = computed<Array<{ id: StatusFilter; label: string; count: number }>>(() => [
|
||||
{
|
||||
id: 'ALL',
|
||||
@@ -152,12 +166,50 @@ const calendarOptions = computed(() => ({
|
||||
buttonText: {
|
||||
today: 'Сегодня',
|
||||
},
|
||||
events: filteredOrders.value.map((order: ManagerOrderItem) => ({
|
||||
id: order.id,
|
||||
title: `${order.code} • ${order.customerId}`,
|
||||
start: new Date(order.createdAt).toISOString(),
|
||||
allDay: true,
|
||||
})),
|
||||
events: filteredOrders.value.map((order: ManagerOrderItem) => {
|
||||
const customer = customerCardMeta(order.customerId);
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
title: formatOrderCode(order.code),
|
||||
start: new Date(order.createdAt).toISOString(),
|
||||
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 } }) => {
|
||||
void router.push(`/client-orders/${event.id}`);
|
||||
},
|
||||
@@ -238,3 +290,82 @@ const calendarOptions = computed(() => ({
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
OrderDetailDocument,
|
||||
type OrderDetailQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
|
||||
import {
|
||||
orderDeliveryStateText,
|
||||
orderLogisticsStateText,
|
||||
@@ -20,6 +21,8 @@ const orderQuery = useQuery(OrderDetailDocument, () => ({
|
||||
const currentOrder = computed<OrderItem | null>(() =>
|
||||
orderQuery.result.value?.order ?? null,
|
||||
);
|
||||
|
||||
const currentOrderCode = computed(() => formatOrderCode(currentOrder.value?.code));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,7 +40,7 @@ const currentOrder = computed<OrderItem | null>(() =>
|
||||
<template v-else>
|
||||
<div class="manager-hero">
|
||||
<p class="manager-eyebrow">Заказ</p>
|
||||
<h1 class="manager-title">{{ currentOrder.code }}</h1>
|
||||
<h1 class="manager-title">{{ currentOrderCode }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MyOrdersDocument,
|
||||
type MyOrdersQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import { formatOrderCode } from '~/composables/useOrderCodePresentation';
|
||||
|
||||
type OrderItem = MyOrdersQuery['myOrders'][number];
|
||||
|
||||
@@ -34,6 +35,7 @@ const filteredOrders = computed(() => {
|
||||
return orders.filter((order) => {
|
||||
const text = [
|
||||
order.code,
|
||||
formatOrderCode(order.code),
|
||||
...order.items.map((item) => item.productName),
|
||||
]
|
||||
.join(' ')
|
||||
|
||||
Reference in New Issue
Block a user