Refine order code and calendar cards
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
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,
|
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">
|
||||||
|
|||||||
@@ -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('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
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) => {
|
||||||
id: order.id,
|
const customer = customerCardMeta(order.customerId);
|
||||||
title: `${order.code} • ${order.customerId}`,
|
|
||||||
start: new Date(order.createdAt).toISOString(),
|
return {
|
||||||
allDay: true,
|
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 } }) => {
|
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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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(' ')
|
||||||
|
|||||||
Reference in New Issue
Block a user