Match Leads stats cards to the mockup and fix Contacts double menus.

Stage summary uses the green total card plus paired/solo white cells; people row menus use separate mobile/desktop keys.
This commit is contained in:
2026-09-27 20:35:57 +03:30
parent 3833788aa8
commit ec5c7639e3
2 changed files with 189 additions and 110 deletions
+4 -4
View File
@@ -406,9 +406,9 @@ onMounted(load);
{{ p.fullName }} {{ p.fullName }}
</RouterLink> </RouterLink>
<UiDropdownMenu <UiDropdownMenu
:open="menuId === p.id" :open="menuId === `m:${p.id}`"
align="end" align="end"
@update:open="(v) => (menuId = v ? p.id : null)" @update:open="(v) => (menuId = v ? `m:${p.id}` : null)"
> >
<template #trigger="{ toggle }"> <template #trigger="{ toggle }">
<button class="more" type="button" @click.stop="toggle"> <button class="more" type="button" @click.stop="toggle">
@@ -491,9 +491,9 @@ onMounted(load);
<td>{{ formatDate(p.createdAt) }}</td> <td>{{ formatDate(p.createdAt) }}</td>
<td class="ops" @click.stop> <td class="ops" @click.stop>
<UiDropdownMenu <UiDropdownMenu
:open="menuId === p.id" :open="menuId === `t:${p.id}`"
align="end" align="end"
@update:open="(v) => (menuId = v ? p.id : null)" @update:open="(v) => (menuId = v ? `t:${p.id}` : null)"
> >
<template #trigger="{ toggle }"> <template #trigger="{ toggle }">
<button type="button" class="more" @click.stop="toggle"> <button type="button" class="more" @click.stop="toggle">
+185 -106
View File
@@ -148,6 +148,13 @@ const stageCards = computed(() => {
return board.slice(0, 5); return board.slice(0, 5);
}); });
const pairStages = computed(() =>
stageCards.value.length >= 2 ? stageCards.value.slice(0, 2) : [],
);
const soloStages = computed(() =>
stageCards.value.length >= 2 ? stageCards.value.slice(2) : stageCards.value,
);
const pageNumbers = computed(() => { const pageNumbers = computed(() => {
const totalPages = pageCount.value; const totalPages = pageCount.value;
const current = page.value; const current = page.value;
@@ -809,31 +816,47 @@ onBeforeUnmount(stopImportPoll);
<div class="stats" role="list"> <div class="stats" role="list">
<button <button
type="button" type="button"
class="stat mint" class="stat-total"
:class="{ on: !stageId }" :class="{ on: !stageId }"
role="listitem" role="listitem"
@click="setStageFilter('')" @click="setStageFilter('')"
> >
<span class="stat-ico"><UiIcon name="users" :size="18" /></span> <span class="stat-total-ico" aria-hidden="true">
<span class="stat-body"> <UiIcon name="users" :size="18" />
</span>
<span class="stat-total-body">
<span class="stat-label">همه لیدها</span> <span class="stat-label">همه لیدها</span>
<strong class="stat-num">{{ formatCount(stats?.total ?? total) }}</strong> <strong class="stat-num">{{ formatCount(stats?.total ?? total) }}</strong>
</span> </span>
</button> </button>
<div v-if="pairStages.length" class="stat-pair" role="listitem">
<button
v-for="s in pairStages"
:key="s.id"
type="button"
class="stat-cell"
:class="{ on: stageId === s.id }"
@click="setStageFilter(s.id)"
>
<i class="stat-dot" :data-tint="stageTint(s.color)" aria-hidden="true" />
<span class="stat-label">{{ s.name }}</span>
<strong class="stat-num">{{ formatCount(s.leadCount ?? 0) }}</strong>
</button>
</div>
<button <button
v-for="s in stageCards" v-for="s in soloStages"
:key="s.id" :key="s.id"
type="button" type="button"
class="stat" class="stat-cell alone"
:class="[stageTint(s.color), { on: stageId === s.id }]" :class="{ on: stageId === s.id }"
role="listitem" role="listitem"
@click="setStageFilter(s.id)" @click="setStageFilter(s.id)"
> >
<span class="stat-ico"><UiIcon name="target" :size="18" /></span> <i class="stat-dot" :data-tint="stageTint(s.color)" aria-hidden="true" />
<span class="stat-body"> <span class="stat-label">{{ s.name }}</span>
<span class="stat-label">{{ s.name }}</span> <strong class="stat-num">{{ formatCount(s.leadCount ?? 0) }}</strong>
<strong class="stat-num">{{ formatCount(s.leadCount ?? 0) }}</strong>
</span>
</button> </button>
</div> </div>
@@ -1289,135 +1312,191 @@ onBeforeUnmount(stopImportPoll);
} }
.stats { .stats {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); flex-wrap: wrap;
gap: 0.65rem; align-items: stretch;
gap: 0.75rem;
} }
.stat { .stat-total {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.7rem; gap: 0.75rem;
text-align: start; min-width: 9.5rem;
padding: 0.85rem 0.95rem; padding: 0.85rem 1rem;
border-radius: 12px; border: 1px solid #bbf7d0;
border: 1px solid transparent; border-radius: 14px;
background: #fff; background: #ecfdf5;
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
color: var(--ink); text-align: start;
color: #047857;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; transition: box-shadow 0.15s, border-color 0.15s;
} }
.stat:hover { .stat-total:hover,
transform: translateY(-1px); .stat-total.on {
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08); box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.22);
border-color: #86efac;
} }
.stat.on { .stat-total-ico {
border-color: color-mix(in srgb, var(--accent) 45%, transparent); width: 2.4rem;
box-shadow: 0 0 0 2px var(--accent-soft); height: 2.4rem;
}
.stat-ico {
width: 2.35rem;
height: 2.35rem;
border-radius: 10px; border-radius: 10px;
background: #fff;
color: #10b981;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
} }
.stat-body { .stat-total-body {
display: grid; display: grid;
gap: 0.15rem; gap: 0.1rem;
min-width: 0; min-width: 0;
} }
.stat-label { .stat-total .stat-label {
font-size: 0.78rem; color: #059669;
color: var(--muted); font-size: 0.82rem;
font-weight: 600;
}
.stat-total .stat-num {
color: #047857;
font-size: 1.35rem;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.15;
}
.stat-pair {
display: flex;
align-items: stretch;
flex: 1 1 14rem;
min-width: 12rem;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.stat-pair .stat-cell {
flex: 1 1 0;
border: 0;
border-radius: 0;
box-shadow: none;
min-width: 0;
}
.stat-pair .stat-cell + .stat-cell {
border-inline-start: 1px solid #e5e7eb;
}
.stat-cell {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.2rem;
min-width: 6.5rem;
padding: 0.85rem 0.9rem 0.75rem;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
cursor: pointer;
font: inherit;
color: #0f172a;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: box-shadow 0.15s, border-color 0.15s;
}
.stat-cell.alone {
flex: 0 1 7.5rem;
}
.stat-cell:hover,
.stat-cell.on {
border-color: color-mix(in srgb, var(--accent) 40%, #e5e7eb);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.stat-pair:has(.stat-cell.on) {
border-color: color-mix(in srgb, var(--accent) 40%, #e5e7eb);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.stat-dot {
width: 0.55rem;
height: 0.55rem;
border-radius: 999px;
margin-bottom: 0.15rem;
background: #94a3b8;
box-shadow: 0 0 0 4px color-mix(in srgb, #94a3b8 18%, transparent);
}
.stat-dot[data-tint='mint'],
.stat-dot[data-tint='teal'] {
background: #10b981;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.18);
}
.stat-dot[data-tint='amber'] {
background: #f59e0b;
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.18);
}
.stat-dot[data-tint='peach'] {
background: #f97316;
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.18);
}
.stat-dot[data-tint='sky'] {
background: #0ea5e9;
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.18);
}
.stat-dot[data-tint='violet'] {
background: #8b5cf6;
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.18);
}
.stat-dot[data-tint='rose'] {
background: #ef4444;
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.18);
}
.stat-cell .stat-label {
font-size: 0.8rem;
font-weight: 500;
color: #64748b;
text-align: center;
white-space: nowrap; white-space: nowrap;
max-width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.stat-num { .stat-cell .stat-num {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 800; font-weight: 800;
color: #0f172a;
letter-spacing: -0.02em; letter-spacing: -0.02em;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
line-height: 1.15;
} }
.stat.mint { @media (max-width: 720px) {
background: linear-gradient(180deg, #f0fdf6 0%, #fff 100%); .stat-pair {
} flex: 1 1 100%;
.stat.mint .stat-ico { }
background: #d1fae5;
color: #059669;
}
.stat.mint .stat-num {
color: #047857;
}
.stat.amber { .stat-cell.alone,
background: linear-gradient(180deg, #fffbeb 0%, #fff 100%); .stat-total {
} flex: 1 1 calc(50% - 0.4rem);
.stat.amber .stat-ico { }
background: #fef3c7;
color: #d97706;
}
.stat.amber .stat-num {
color: #b45309;
}
.stat.peach {
background: linear-gradient(180deg, #fff7ed 0%, #fff 100%);
}
.stat.peach .stat-ico {
background: #ffedd5;
color: #ea580c;
}
.stat.peach .stat-num {
color: #c2410c;
}
.stat.sky,
.stat.teal {
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
}
.stat.sky .stat-ico,
.stat.teal .stat-ico {
background: #ccfbf1;
color: #0d9488;
}
.stat.sky .stat-num,
.stat.teal .stat-num {
color: #0f766e;
}
.stat.violet {
background: linear-gradient(180deg, #f5f3ff 0%, #fff 100%);
}
.stat.violet .stat-ico {
background: #ede9fe;
color: #7c3aed;
}
.stat.violet .stat-num {
color: #6d28d9;
}
.stat.rose {
background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
}
.stat.rose .stat-ico {
background: #fee2e2;
color: #dc2626;
}
.stat.rose .stat-num {
color: #b91c1c;
} }
.filters { .filters {