Files
web-frontend/app/components/orders/OrderSummaryCard.vue
2026-04-04 13:52:01 +07:00

178 lines
5.9 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 OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
import { formatPrice } from '~/composables/useOrderDetailPresentation';
type OrderCardItem = {
id: string;
productName: string;
quantity: number;
};
type CustomerCardMeta = {
name?: string | null;
avatarSrc?: string | null;
initials?: string | null;
};
const props = defineProps<{
to: string;
code: string;
status: string;
createdAt: string | Date;
totalPrice?: number | null;
items: OrderCardItem[];
customer?: CustomerCardMeta | null;
}>();
const DATE_FORMATTER = new Intl.DateTimeFormat('ru-RU', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
});
const coverPresets = [
['#d9f5e6', '#9ce8c1', '#6fd09d'],
['#eaf9ef', '#b3e8cb', '#76c89f'],
['#e8f5ec', '#b2e0c6', '#7dd0a9'],
];
function formatCreatedAt(value: string | Date) {
return DATE_FORMATTER.format(new Date(value));
}
function buildInitials(value?: string | null) {
const parts = String(value ?? '')
.trim()
.split(/\s+/)
.filter(Boolean)
.slice(0, 2);
if (!parts.length) {
return 'FR';
}
return parts.map((part) => part.charAt(0).toUpperCase()).join('');
}
function itemCountLabel(count: number) {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) {
return `${count} позиция`;
}
if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
return `${count} позиции`;
}
return `${count} позиций`;
}
function createProductCover(name: string, seedKey: string) {
const seed = `${name}${seedKey}`.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const [start, middle, finish] = coverPresets[seed % coverPresets.length];
const firstLetter = name.trim().charAt(0).toUpperCase() || 'P';
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88 88">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${start}" />
<stop offset="55%" stop-color="${middle}" />
<stop offset="100%" stop-color="${finish}" />
</linearGradient>
</defs>
<rect width="88" height="88" fill="url(#g)" rx="24" />
<g opacity="0.16">
<circle cx="72" cy="18" r="20" fill="#0f7a49" />
<circle cx="14" cy="80" r="22" fill="#0f7a49" />
</g>
<text x="50%" y="56%" text-anchor="middle" fill="#11412c" font-family="Manrope, sans-serif" font-size="34" font-weight="700">${firstLetter}</text>
</svg>
`.trim();
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
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 customerName = computed(() => String(props.customer?.name ?? '').trim());
const customerInitials = computed(() => (
String(props.customer?.initials ?? '').trim() || buildInitials(customerName.value)
));
</script>
<template>
<NuxtLink
:to="to"
class="surface-card group block rounded-[30px] border border-[#dbece2] bg-white px-4 py-4 transition hover:-translate-y-0.5 hover:shadow-[0_18px_40px_rgba(18,56,36,0.08)] md:px-5"
>
<div class="flex items-center gap-4">
<div v-if="customer" class="shrink-0">
<img
v-if="customer?.avatarSrc"
:src="customer.avatarSrc"
:alt="customerName || code"
class="h-14 w-14 rounded-[20px] object-cover"
>
<div
v-else
class="flex h-14 w-14 items-center justify-center rounded-[20px] bg-[linear-gradient(135deg,#dff7e9_0%,#c2ead3_100%)] text-base font-black text-[#123824]"
>
{{ customerInitials }}
</div>
</div>
<div class="min-w-0 flex-1">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<h2 class="text-lg font-bold text-[#123824]">{{ code }}</h2>
<p class="text-sm text-[#688676]">{{ formatCreatedAt(createdAt) }}</p>
</div>
<p v-if="customerName" class="mt-1 truncate text-sm font-semibold text-[#355947]">
{{ customerName }}
</p>
</div>
<div class="flex flex-wrap items-center gap-3 lg:justify-end">
<div class="text-left lg:text-right">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">Сумма</p>
<p class="text-sm font-bold text-[#123824]">{{ totalPriceLabel }}</p>
</div>
<OrderStatusBadge :status="status" />
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-3">
<div class="flex -space-x-3">
<div
v-for="item in visibleItems"
:key="item.id"
class="h-11 w-11 overflow-hidden rounded-[16px] border-2 border-white bg-[#edf8f1] shadow-[0_6px_16px_rgba(18,56,36,0.08)]"
:title="`${item.productName} × ${item.quantity}`"
>
<img
:src="createProductCover(item.productName, item.id)"
:alt="item.productName"
class="h-full w-full object-cover"
>
</div>
<div
v-if="hiddenCount > 0"
class="flex h-11 w-11 items-center justify-center rounded-[16px] border-2 border-white bg-[#123824] text-xs font-bold text-white shadow-[0_6px_16px_rgba(18,56,36,0.14)]"
>
+{{ hiddenCount }}
</div>
</div>
<p class="text-sm text-[#5c7b69]">
{{ itemCountLabel(items.length) }}
</p>
</div>
</div>
</div>
</NuxtLink>
</template>