Tighten Users filters and admin-only header badges.

Match sticky headers to sidebar glass, drop the Users role column, and show compact owner/admin badges only where needed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-11 00:23:39 +03:30
co-authored by Cursor
parent 6c52ce1247
commit 722933dda9
11 changed files with 160 additions and 86 deletions
@@ -77,11 +77,12 @@
.roleBadge { .roleBadge {
display: inline-block; display: inline-block;
margin-top: 4px; margin-top: 2px;
padding: 2px 8px; padding: 0 5px;
border-radius: 999px; border-radius: 999px;
font-size: 11px; font-size: 9px;
font-weight: 600; font-weight: 600;
line-height: 1.5;
color: var(--primary); color: var(--primary);
background: rgba(var(--primary-rgb) / 0.1); background: rgba(var(--primary-rgb) / 0.1);
border: 1px solid rgba(var(--primary-rgb) / 0.18); border: 1px solid rgba(var(--primary-rgb) / 0.18);
+17 -7
View File
@@ -6,11 +6,11 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 50; z-index: 50;
background: color-mix(in srgb, #ffffff 42%, transparent); background: var(--glass-bg);
backdrop-filter: saturate(180%) blur(var(--blur-glass, 28px)); backdrop-filter: blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(var(--blur-glass, 28px)); -webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
/* Keep sticky header on its own compositor layer so backdrop blur samples content beneath */ isolation: isolate;
transform: translateZ(0); transform: translateZ(0);
} }
@@ -112,9 +112,19 @@
line-height: 1.2; line-height: 1.2;
} }
.role { .roleBadge {
font-size: 12px; display: inline-block;
color: var(--text-muted); margin-top: 2px;
padding: 1px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
line-height: 1.4;
color: var(--primary);
background: rgba(var(--primary-rgb) / 0.1);
border: 1px solid rgba(var(--primary-rgb) / 0.18);
font-family: var(--font-ui);
width: fit-content;
} }
.chevron { .chevron {
+14 -8
View File
@@ -4,6 +4,7 @@ import { Menu, Bell, MessageSquare, ChevronDown, User, Settings, KeyRound, LogOu
import { LanguageSelect, PasswordResetModal, useLocale } from '@meshkee/dashboard-ui' import { LanguageSelect, PasswordResetModal, useLocale } from '@meshkee/dashboard-ui'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import { useT } from '../i18n/useT' import { useT } from '../i18n/useT'
import { getActiveBusinessId } from '../lib/businessContext'
import { changePassword } from '../services/authService' import { changePassword } from '../services/authService'
import styles from './Header.module.css' import styles from './Header.module.css'
@@ -40,13 +41,16 @@ export function Header() {
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
const displayName = displayUserName(user, locale, t('app.userFallback')) const displayName = displayUserName(user, locale, t('app.userFallback'))
const roleLabel = const isSuperAdmin = Boolean(user?.isSuperAdmin || user?.roles.includes('super_admin'))
user?.roleLabel ?? const activeBusinessId = getActiveBusinessId()
(user?.isSuperAdmin || user?.roles.includes('super_admin') const membership =
? t('role.superAdmin') user?.businesses.find((b) => String(b.id) === String(activeBusinessId)) ??
: user?.businesses[0]?.isOwner user?.businesses[0]
? t('role.owner') const adminBadgeLabel = isSuperAdmin
: user?.businesses[0]?.teamRole ?? t('role.staff')) ? t('role.superAdmin')
: membership?.teamRole === 'admin'
? t('role.admin')
: null
useEffect(() => { useEffect(() => {
if (!menuOpen) return if (!menuOpen) return
@@ -112,7 +116,9 @@ export function Header() {
> >
<div className={styles.profileInfo}> <div className={styles.profileInfo}>
<span className={styles.name}>{displayName}</span> <span className={styles.name}>{displayName}</span>
<span className={styles.role}>{roleLabel}</span> {adminBadgeLabel ? (
<span className={styles.roleBadge}>{adminBadgeLabel}</span>
) : null}
</div> </div>
<ChevronDown <ChevronDown
size={16} size={16}
@@ -41,6 +41,18 @@
align-self: center; align-self: center;
} }
.filterActionsCol1 {
grid-column: span 1;
justify-content: flex-start;
gap: 6px;
}
.filterSpacerCol4 {
grid-column: span 4;
min-width: 0;
pointer-events: none;
}
.field { .field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -56,6 +68,10 @@
min-width: 88px; min-width: 88px;
} }
.fieldCol1 {
grid-column: span 1;
}
.fieldCol3 { .fieldCol3 {
grid-column: span 3; grid-column: span 3;
} }
@@ -64,6 +80,10 @@
grid-column: span 4; grid-column: span 4;
} }
.fieldCol8 {
grid-column: span 8;
}
.field label { .field label {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
@@ -187,9 +207,20 @@
justify-content: flex-end; justify-content: flex-end;
} }
.filterActionsCol1 {
grid-column: span 12;
justify-content: flex-end;
}
.filterSpacerCol4 {
display: none;
}
.fieldCol1,
.fieldCol2, .fieldCol2,
.fieldCol3, .fieldCol3,
.fieldCol4 { .fieldCol4,
.fieldCol8 {
grid-column: span 6; grid-column: span 6;
} }
} }
@@ -199,9 +230,11 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.fieldCol1,
.fieldCol2, .fieldCol2,
.fieldCol3, .fieldCol3,
.fieldCol4 { .fieldCol4,
.fieldCol8 {
grid-column: span 1; grid-column: span 1;
} }
} }
+2
View File
@@ -8,6 +8,7 @@ const en = {
'role.superAdmin': 'Super Admin', 'role.superAdmin': 'Super Admin',
'role.owner': 'Business Owner', 'role.owner': 'Business Owner',
'role.admin': 'Admin',
'role.staff': 'Staff', 'role.staff': 'Staff',
'nav.home': 'Home', 'nav.home': 'Home',
@@ -1383,6 +1384,7 @@ const fa: Record<MessageKey, string> = {
'role.superAdmin': 'سوپرادمین', 'role.superAdmin': 'سوپرادمین',
'role.owner': 'صاحب کسب‌وکار', 'role.owner': 'صاحب کسب‌وکار',
'role.admin': 'ادمین',
'role.staff': 'کارمند', 'role.staff': 'کارمند',
'nav.home': 'خانه', 'nav.home': 'خانه',
@@ -41,27 +41,23 @@
} }
.colName { .colName {
width: 18%; width: 24%;
}
.colRole {
width: 16%;
} }
.colCell { .colCell {
width: 13%; width: 16%;
} }
.colOrders { .colOrders {
width: 10%;
}
.colTransactions {
width: 12%; width: 12%;
} }
.colTransactions {
width: 14%;
}
.colDate { .colDate {
width: 11%; width: 12%;
} }
.colActions { .colActions {
+41 -46
View File
@@ -33,7 +33,7 @@ import accessStyles from '../components/ChangeUserAccessModal.module.css'
import styles from './CustomersPage.module.css' import styles from './CustomersPage.module.css'
const PAGE_SIZE = 24 const PAGE_SIZE = 24
const COLUMN_COUNT = 7 const COLUMN_COUNT = 6
function formatDate(value: string, locale: 'en' | 'fa') { function formatDate(value: string, locale: 'en' | 'fa') {
const d = new Date(value) const d = new Date(value)
@@ -54,13 +54,11 @@ function formatTransactionTotal(total: number | null | undefined) {
return formatIrtPrice(total) return formatIrtPrice(total)
} }
function accessBadgeKey(user: BusinessCustomerListItem): BusinessMessageKey { /** Badge under name — owner / admin only. */
function adminBadgeKey(user: BusinessCustomerListItem): BusinessMessageKey | null {
if (user.isBusinessOwner) return 'customers.access.badge.owner' if (user.isBusinessOwner) return 'customers.access.badge.owner'
if (user.teamRole === 'admin') return 'customers.access.badge.admin' if (user.teamRole === 'admin') return 'customers.access.badge.admin'
if (user.teamRole === 'editor') return 'customers.access.badge.editor' return null
if (user.teamRole === 'viewer') return 'customers.access.badge.viewer'
if (user.businessMemberId) return 'customers.access.badge.manager'
return 'customers.access.customer'
} }
function teamRoleMessageKey(teamRole: string): BusinessMessageKey { function teamRoleMessageKey(teamRole: string): BusinessMessageKey {
@@ -322,41 +320,40 @@ export function CustomersPage() {
<div className={filterStyles.filtersPanel}> <div className={filterStyles.filtersPanel}>
<form className={filterStyles.filtersGrid} onSubmit={(e) => { e.preventDefault(); applyFilters() }}> <form className={filterStyles.filtersGrid} onSubmit={(e) => { e.preventDefault(); applyFilters() }}>
<div className={filterStyles.filtersInputs}> <div className={`${filterStyles.field} ${filterStyles.fieldCol3}`}>
<div className={`${filterStyles.field} ${filterStyles.fieldCol3}`}> <input
<select id="filter-customer-name"
id="filter-customer-access" value={draftName}
value={draftAccess} onChange={(e) => setDraftName(e.target.value)}
onChange={(e) => handleAccessFilterChange(e.target.value as CustomerAccessFilter)} placeholder={t('customers.filter.name')}
aria-label={t('customers.filter.access')} aria-label={t('customers.filter.name')}
disabled={loading} />
>
<option value="all">{t('customers.filter.access.all')}</option>
<option value="customers">{t('customers.filter.access.customers')}</option>
<option value="managers">{t('customers.filter.access.managers')}</option>
</select>
</div>
<div className={`${filterStyles.field} ${filterStyles.fieldCol3}`}>
<input
id="filter-customer-name"
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
placeholder={t('customers.filter.name')}
aria-label={t('customers.filter.name')}
/>
</div>
<div className={`${filterStyles.field} ${styles.filterCellNarrow}`}>
<input
id="filter-customer-cell"
value={draftCell}
onChange={(e) => setDraftCell(e.target.value)}
placeholder={t('customers.filter.cell')}
aria-label={t('customers.filter.cell')}
autoComplete="off"
/>
</div>
</div> </div>
<div className={filterStyles.filterActions}> <div className={`${filterStyles.field} ${filterStyles.fieldCol2}`}>
<input
id="filter-customer-cell"
value={draftCell}
onChange={(e) => setDraftCell(e.target.value)}
placeholder={t('customers.filter.cell')}
aria-label={t('customers.filter.cell')}
autoComplete="off"
/>
</div>
<div className={`${filterStyles.field} ${filterStyles.fieldCol2}`}>
<select
id="filter-customer-access"
value={draftAccess}
onChange={(e) => handleAccessFilterChange(e.target.value as CustomerAccessFilter)}
aria-label={t('customers.filter.access')}
disabled={loading}
>
<option value="all">{t('customers.filter.access.all')}</option>
<option value="customers">{t('customers.filter.access.customers')}</option>
<option value="managers">{t('customers.filter.access.managers')}</option>
</select>
</div>
<div className={filterStyles.filterSpacerCol4} aria-hidden="true" />
<div className={`${filterStyles.filterActions} ${filterStyles.filterActionsCol1}`}>
<button <button
type="submit" type="submit"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnPrimary}`} className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnPrimary}`}
@@ -408,7 +405,6 @@ export function CustomersPage() {
> >
<colgroup> <colgroup>
<col className={styles.colName} /> <col className={styles.colName} />
<col className={styles.colRole} />
<col className={styles.colCell} /> <col className={styles.colCell} />
<col className={styles.colOrders} /> <col className={styles.colOrders} />
<col className={styles.colTransactions} /> <col className={styles.colTransactions} />
@@ -418,7 +414,6 @@ export function CustomersPage() {
<thead> <thead>
<tr> <tr>
<th className={styles.th}>{t('customers.col.name')}</th> <th className={styles.th}>{t('customers.col.name')}</th>
<th className={styles.th}>{t('customers.col.role')}</th>
<th className={styles.th}>{t('customers.col.cell')}</th> <th className={styles.th}>{t('customers.col.cell')}</th>
<th className={styles.th}>{t('customers.col.orders')}</th> <th className={styles.th}>{t('customers.col.orders')}</th>
<th className={styles.th}>{t('customers.col.transactions')}</th> <th className={styles.th}>{t('customers.col.transactions')}</th>
@@ -447,7 +442,7 @@ export function CustomersPage() {
data?.items?.map((customer) => { data?.items?.map((customer) => {
const name = displayName(customer) const name = displayName(customer)
const nameLocale = textLocaleAttrs(name) const nameLocale = textLocaleAttrs(name)
const badgeKey = accessBadgeKey(customer) const badgeKey = adminBadgeKey(customer)
const accessLocked = accessChangeDisabledReason(customer) const accessLocked = accessChangeDisabledReason(customer)
return ( return (
<tr <tr
@@ -468,13 +463,13 @@ export function CustomersPage() {
> >
{name} {name}
</div> </div>
{badgeKey ? (
<span className={accessStyles.roleBadge}>{t(badgeKey)}</span>
) : null}
{!customer.isEnabled && ( {!customer.isEnabled && (
<div className={styles.statusDisabled}>{t('customers.disabled')}</div> <div className={styles.statusDisabled}>{t('customers.disabled')}</div>
)} )}
</td> </td>
<td className={styles.td}>
<span className={accessStyles.roleBadge}>{t(badgeKey)}</span>
</td>
<td className={styles.td}>{formatCellForDisplay(customer.cellNumber)}</td> <td className={styles.td}>{formatCellForDisplay(customer.cellNumber)}</td>
<td className={styles.td}> <td className={styles.td}>
{customer.orderCount == null || customer.orderCount === 0 ? ( {customer.orderCount == null || customer.orderCount === 0 ? (
+19 -3
View File
@@ -6,10 +6,11 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 50; z-index: 50;
background: color-mix(in srgb, #ffffff 42%, transparent); background: var(--glass-bg);
backdrop-filter: saturate(180%) blur(var(--blur-glass, 28px)); backdrop-filter: blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(var(--blur-glass, 28px)); -webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
isolation: isolate;
transform: translateZ(0); transform: translateZ(0);
} }
@@ -182,6 +183,21 @@
color: var(--text-muted); color: var(--text-muted);
} }
.roleBadge {
display: inline-block;
margin-top: 2px;
padding: 1px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
line-height: 1.4;
color: var(--primary);
background: rgba(var(--primary-rgb) / 0.1);
border: 1px solid rgba(var(--primary-rgb) / 0.18);
font-family: var(--font-ui);
width: fit-content;
}
.chevron { .chevron {
color: var(--text-muted); color: var(--text-muted);
transition: transform 0.2s ease; transition: transform 0.2s ease;
+13 -3
View File
@@ -60,9 +60,17 @@ export function Header({ hideMenu = false, showBrand = false }: HeaderProps) {
: (businessNameEn && businessNameEn !== brandTitle ? businessNameEn : '') : (businessNameEn && businessNameEn !== brandTitle ? businessNameEn : '')
const activeBusinessId = getActiveBusinessId() const activeBusinessId = getActiveBusinessId()
const isSuperAdmin = Boolean(user?.isSuperAdmin || user?.roles.includes('super_admin'))
const membership = user?.businesses.find(
(business) => String(business.id) === String(activeBusinessId),
)
const adminBadgeLabel = isSuperAdmin
? t('app.role.superAdmin')
: membership?.teamRole === 'admin'
? t('app.role.admin')
: null
const canOpenBusinessDashboard = Boolean( const canOpenBusinessDashboard = Boolean(
user?.isSuperAdmin || isSuperAdmin ||
user?.roles.includes('super_admin') ||
user?.businesses.some( user?.businesses.some(
(business) => (business) =>
String(business.id) === String(activeBusinessId) && String(business.id) === String(activeBusinessId) &&
@@ -162,7 +170,9 @@ export function Header({ hideMenu = false, showBrand = false }: HeaderProps) {
> >
<div className={styles.profileInfo}> <div className={styles.profileInfo}>
<span className={styles.name}>{displayName}</span> <span className={styles.name}>{displayName}</span>
<span className={styles.role}>{t('app.role.customer')}</span> {adminBadgeLabel ? (
<span className={styles.roleBadge}>{adminBadgeLabel}</span>
) : null}
</div> </div>
<ChevronDown <ChevronDown
size={16} size={16}
+4
View File
@@ -3,6 +3,8 @@ import type { DashboardLocale } from '@meshkee/dashboard-core'
const en = { const en = {
'app.dashboardName': 'Customer Dashboard', 'app.dashboardName': 'Customer Dashboard',
'app.role.customer': 'Website user', 'app.role.customer': 'Website user',
'app.role.superAdmin': 'Super Admin',
'app.role.admin': 'Admin',
'app.storeFallback': 'Store', 'app.storeFallback': 'Store',
'app.poweredBy': 'powered by Meshkee.app', 'app.poweredBy': 'powered by Meshkee.app',
@@ -389,6 +391,8 @@ type MessageKey = keyof typeof en
const fa: Record<MessageKey, string> = { const fa: Record<MessageKey, string> = {
'app.dashboardName': 'پنل مشتری', 'app.dashboardName': 'پنل مشتری',
'app.role.customer': 'کاربر وبسایت', 'app.role.customer': 'کاربر وبسایت',
'app.role.superAdmin': 'سوپرادمین',
'app.role.admin': 'ادمین',
'app.storeFallback': 'فروشگاه', 'app.storeFallback': 'فروشگاه',
'app.poweredBy': 'قدرت‌گرفته از Meshkee.app', 'app.poweredBy': 'قدرت‌گرفته از Meshkee.app',
@@ -6,10 +6,11 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 50; z-index: 50;
background: color-mix(in srgb, #ffffff 42%, transparent); background: var(--glass-bg);
backdrop-filter: saturate(180%) blur(var(--blur-glass, 28px)); backdrop-filter: blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(var(--blur-glass, 28px)); -webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
isolation: isolate;
transform: translateZ(0); transform: translateZ(0);
} }