Refine order list layout and client card
This commit is contained in:
@@ -14,14 +14,17 @@ const statusLabel = computed(() => {
|
||||
});
|
||||
|
||||
const className = computed(() => {
|
||||
if (props.status === 'COMPLETED') return 'badge badge-success';
|
||||
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'badge badge-error';
|
||||
if (props.status === 'MANAGER_BLOCKED') return 'badge badge-warning';
|
||||
if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'badge badge-warning';
|
||||
return 'badge badge-info';
|
||||
if (props.status === 'COMPLETED') return 'bg-[#139957]';
|
||||
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'bg-[#d94b55]';
|
||||
if (props.status === 'MANAGER_BLOCKED') return 'bg-[#f1a43a]';
|
||||
if (props.status === 'NEW' || props.status === 'MANAGER_PROCESSING') return 'bg-[#f1a43a]';
|
||||
return 'bg-[#2e8de4]';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="className">{{ statusLabel }}</span>
|
||||
<span class="inline-flex items-center gap-2 text-sm font-semibold text-[#123824]">
|
||||
<span class="h-2.5 w-2.5 rounded-full" :class="className" />
|
||||
<span>{{ statusLabel }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -8,12 +8,6 @@ type OrderCardItem = {
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
type CustomerCardMeta = {
|
||||
name?: string | null;
|
||||
avatarSrc?: string | null;
|
||||
initials?: string | null;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
to: string;
|
||||
code: string;
|
||||
@@ -21,7 +15,6 @@ const props = defineProps<{
|
||||
createdAt: string | Date;
|
||||
totalPrice?: number | null;
|
||||
items: OrderCardItem[];
|
||||
customer?: CustomerCardMeta | null;
|
||||
}>();
|
||||
|
||||
const DATE_FORMATTER = new Intl.DateTimeFormat('ru-RU', {
|
||||
@@ -41,33 +34,6 @@ 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];
|
||||
@@ -96,11 +62,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 customerName = computed(() => String(props.customer?.name ?? '').trim());
|
||||
const customerInitials = computed(() => (
|
||||
String(props.customer?.initials ?? '').trim() || buildInitials(customerName.value)
|
||||
));
|
||||
const totalPriceLabel = computed(() => formatPrice(props.totalPrice) ?? '—');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,44 +70,14 @@ const customerInitials = computed(() => (
|
||||
: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="grid gap-4 md:grid-cols-4 md:items-center md:gap-6">
|
||||
<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>
|
||||
<h2 class="truncate text-lg font-bold text-[#123824]">{{ code }}</h2>
|
||||
<p class="mt-1 text-sm text-[#688676]">{{ formatCreatedAt(createdAt) }}</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="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 md:gap-1">
|
||||
<div class="flex -space-x-3">
|
||||
<div
|
||||
v-for="item in visibleItems"
|
||||
@@ -159,18 +91,22 @@ const customerInitials = computed(() => (
|
||||
class="h-full w-full object-cover"
|
||||
>
|
||||
</div>
|
||||
</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)]"
|
||||
class="flex h-11 w-11 items-center justify-center rounded-[16px] 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 class="flex items-center md:justify-center">
|
||||
<OrderStatusBadge :status="status" />
|
||||
</div>
|
||||
|
||||
<div class="text-left md:text-right">
|
||||
<p class="text-base font-bold text-[#123824]">{{ totalPriceLabel }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -21,7 +21,7 @@ function updateValue(event: Event) {
|
||||
<slot name="tabs" />
|
||||
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
<label class="input input-bordered flex w-full flex-1 items-center gap-3 rounded-full bg-white">
|
||||
<label class="flex w-full flex-1 items-center gap-3 rounded-full border border-[#d7e9de] bg-white px-4 py-3 transition focus-within:border-[#139957] focus-within:shadow-[0_0_0_3px_rgba(19,153,87,0.12)]">
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0 text-base-content/45"
|
||||
viewBox="0 0 20 20"
|
||||
@@ -46,7 +46,7 @@ function updateValue(event: Event) {
|
||||
<input
|
||||
:value="props.modelValue"
|
||||
type="text"
|
||||
class="grow"
|
||||
class="min-w-0 grow bg-transparent text-sm text-[#123824] outline-none placeholder:text-[#7b9487]"
|
||||
:placeholder="props.searchPlaceholder"
|
||||
@input="updateValue"
|
||||
>
|
||||
|
||||
@@ -207,14 +207,26 @@ const calendarOptions = computed(() => ({
|
||||
>
|
||||
<template #controls>
|
||||
<div class="flex w-full flex-col gap-3 md:w-auto md:flex-row">
|
||||
<div class="tabs tabs-boxed w-fit bg-white">
|
||||
<button class="tab" :class="{ 'tab-active': viewMode === 'cards' }" @click="setViewMode('cards')">
|
||||
<div class="inline-flex w-fit rounded-full border border-[#d7e9de] bg-white p-1">
|
||||
<button
|
||||
class="rounded-full px-4 py-2 text-sm font-semibold text-[#355947] transition"
|
||||
:class="viewMode === 'cards' ? 'bg-[#123824] text-white' : 'hover:bg-[#f4faf6]'"
|
||||
@click="setViewMode('cards')"
|
||||
>
|
||||
Карточки
|
||||
</button>
|
||||
<button class="tab" :class="{ 'tab-active': viewMode === 'calendar' }" @click="setViewMode('calendar')">
|
||||
<button
|
||||
class="rounded-full px-4 py-2 text-sm font-semibold text-[#355947] transition"
|
||||
:class="viewMode === 'calendar' ? 'bg-[#123824] text-white' : 'hover:bg-[#f4faf6]'"
|
||||
@click="setViewMode('calendar')"
|
||||
>
|
||||
Календарь
|
||||
</button>
|
||||
<button class="tab" :class="{ 'tab-active': viewMode === 'kanban' }" @click="setViewMode('kanban')">
|
||||
<button
|
||||
class="rounded-full px-4 py-2 text-sm font-semibold text-[#355947] transition"
|
||||
:class="viewMode === 'kanban' ? 'bg-[#123824] text-white' : 'hover:bg-[#f4faf6]'"
|
||||
@click="setViewMode('kanban')"
|
||||
>
|
||||
Kanban
|
||||
</button>
|
||||
</div>
|
||||
@@ -288,7 +300,6 @@ const calendarOptions = computed(() => ({
|
||||
:created-at="order.createdAt"
|
||||
:total-price="order.totalPrice"
|
||||
:items="order.items"
|
||||
:customer="customerCardMeta(order.customerId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +315,6 @@ const calendarOptions = computed(() => ({
|
||||
:created-at="order.createdAt"
|
||||
:total-price="order.totalPrice"
|
||||
:items="order.items"
|
||||
:customer="customerCardMeta(order.customerId)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -148,8 +148,8 @@ async function rejectRequest() {
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="surface-card flex flex-col gap-6 rounded-[36px] p-6 md:flex-row md:items-center">
|
||||
<div class="shrink-0">
|
||||
<div class="surface-card rounded-[36px] p-6">
|
||||
<div class="flex flex-col items-center gap-4 text-center">
|
||||
<img
|
||||
v-if="messengerConnectionAvatarSrc(currentUser.telegramConnection)"
|
||||
:src="messengerConnectionAvatarSrc(currentUser.telegramConnection)"
|
||||
@@ -162,15 +162,17 @@ async function rejectRequest() {
|
||||
>
|
||||
{{ userInitials(currentUser.fullName) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="manager-eyebrow">Пользователь</p>
|
||||
<h1 class="manager-title">{{ currentUser.fullName }}</h1>
|
||||
<h1 class="text-[clamp(1.8rem,3vw,2.8rem)] font-black leading-none tracking-[-0.04em] text-[#123824]">
|
||||
{{ currentUser.fullName }}
|
||||
</h1>
|
||||
<p class="text-sm text-[#466653]">{{ currentUser.email }}</p>
|
||||
<p v-if="currentUser.companyName" class="text-sm text-[#466653]">{{ currentUser.companyName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="surface-card rounded-3xl p-5">
|
||||
<h2 class="text-xl font-bold text-[#123824]">Заказы пользователя</h2>
|
||||
@@ -187,11 +189,6 @@ async function rejectRequest() {
|
||||
:created-at="order.createdAt"
|
||||
:total-price="order.totalPrice"
|
||||
:items="order.items"
|
||||
:customer="{
|
||||
name: currentUser.fullName,
|
||||
avatarSrc: messengerConnectionAvatarSrc(currentUser.telegramConnection),
|
||||
initials: userInitials(currentUser.fullName),
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,10 @@ const filteredOrders = computed(() => {
|
||||
search-placeholder="Номер заказа или товар"
|
||||
>
|
||||
<template #controls>
|
||||
<select v-model="statusFilter" class="select select-bordered w-full rounded-full bg-white md:w-64">
|
||||
<select
|
||||
v-model="statusFilter"
|
||||
class="w-full rounded-full border border-[#d7e9de] bg-white px-4 py-3 text-sm font-semibold text-[#123824] outline-none transition focus:border-[#139957] focus:shadow-[0_0_0_3px_rgba(19,153,87,0.12)] md:w-64"
|
||||
>
|
||||
<option value="ALL">Все заказы</option>
|
||||
<option value="WAITING">Ожидают подтверждения</option>
|
||||
<option value="ACTIVE">Активные</option>
|
||||
@@ -62,8 +65,10 @@ const filteredOrders = computed(() => {
|
||||
</template>
|
||||
</UiSectionSearchHero>
|
||||
|
||||
<div v-if="allOrders.loading.value" class="alert surface-card border-0">Загрузка заказов...</div>
|
||||
<div v-else-if="filteredOrders.length === 0" class="alert surface-card border-0">
|
||||
<div v-if="allOrders.loading.value" class="manager-empty-state">
|
||||
Загрузка заказов...
|
||||
</div>
|
||||
<div v-else-if="filteredOrders.length === 0" class="manager-empty-state">
|
||||
Заказы по текущим условиям не найдены.
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user