From e79211e3cdf9d0128194b0d07ab0de2be3498430 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sun, 16 Aug 2026 01:45:41 +0330 Subject: [PATCH] Move e-payment to Settings and add ZarinPal credentials. Gateway config now lives on the general Settings page, with Mellat fields in a 3-column layout and ZarinPal available at checkout. Co-authored-by: Cursor --- apps/business/src/App.tsx | 7 +- .../src/components/CategoryModal.module.css | 13 + .../CategorySearchSelect.module.css | 10 +- .../components/CitySearchSelect.module.css | 10 +- .../components/MultiSelectDropdown.module.css | 12 +- .../src/components/PageContent.module.css | 19 +- .../components/SearchableSelect.module.css | 16 +- apps/business/src/components/SectionCard.tsx | 4 +- apps/business/src/components/Sidebar.tsx | 1 - .../src/components/StoreItemDiscountModal.tsx | 30 ++- .../StoreItemMultiSearchSelect.module.css | 4 - .../components/StoreItemMultiSearchSelect.tsx | 4 +- .../src/components/StoreSpecialModal.tsx | 96 ++++++-- .../src/components/VariationsModal.module.css | 22 +- apps/business/src/i18n/messages.ts | 72 +++--- apps/business/src/index.css | 6 + apps/business/src/pages/HomePage.tsx | 4 + ...age.module.css => SettingsPage.module.css} | 9 +- ...bsiteEPaymentPage.tsx => SettingsPage.tsx} | 223 +++++++++++++++--- apps/business/src/pages/StoreItemsPage.tsx | 1 + apps/business/src/pages/StoreSettingsPage.tsx | 6 +- apps/business/src/pages/StoreSpecialsPage.tsx | 24 +- apps/business/src/pages/WebsitePage.tsx | 8 - apps/business/src/services/settingsService.ts | 14 +- .../CategorySearchSelect.module.css | 10 +- .../components/CitySearchSelect.module.css | 10 +- apps/customer/src/index.css | 5 + .../pages/checkout/CheckoutPaymentStep.tsx | 84 ++++--- apps/customer/src/services/cartService.ts | 27 +++ .../src/components/Header.module.css | 3 +- apps/super-admin/src/index.css | 5 + .../src/pages/UsersPage.module.css | 24 +- apps/super-admin/src/pages/UsersPage.tsx | 37 ++- .../src/pages/WebsitesPage.module.css | 2 +- docs/PROJECT_CONTEXT.md | 4 +- 35 files changed, 614 insertions(+), 212 deletions(-) rename apps/business/src/pages/{WebsiteEPaymentPage.module.css => SettingsPage.module.css} (95%) rename apps/business/src/pages/{WebsiteEPaymentPage.tsx => SettingsPage.tsx} (61%) diff --git a/apps/business/src/App.tsx b/apps/business/src/App.tsx index 03cbc89..26ddf69 100644 --- a/apps/business/src/App.tsx +++ b/apps/business/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { BrowserRouter, Navigate, Routes, Route } from 'react-router-dom' import { BusinessDomainGuard } from './components/BusinessDomainGuard' import { DashboardDocumentTitle } from './components/DashboardDocumentTitle' import { AuthProvider } from './context/AuthContext' @@ -66,7 +66,7 @@ import { WebsiteContactPage } from './pages/WebsiteContactPage' import { WebsiteSubscriptionsPage } from './pages/WebsiteSubscriptionsPage' import { WebsiteFaqPage } from './pages/WebsiteFaqPage' import { WebsiteBadgesPage } from './pages/WebsiteBadgesPage' -import { WebsiteEPaymentPage } from './pages/WebsiteEPaymentPage' +import { SettingsPage } from './pages/SettingsPage' import { WebsiteSlidersPage } from './pages/WebsiteSlidersPage' import { WebsiteSpecialCategoriesPage } from './pages/WebsiteSpecialCategoriesPage' import { WebsiteSpecialBrandsPage } from './pages/WebsiteSpecialBrandsPage' @@ -91,6 +91,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> @@ -155,7 +156,7 @@ function App() { } /> } /> } /> - } /> + } /> diff --git a/apps/business/src/components/CategoryModal.module.css b/apps/business/src/components/CategoryModal.module.css index 65f57e0..42ffe7b 100644 --- a/apps/business/src/components/CategoryModal.module.css +++ b/apps/business/src/components/CategoryModal.module.css @@ -152,6 +152,19 @@ box-shadow: 0 6px 16px rgba(var(--primary-rgb) / 0.35); } +.submitBtn:disabled { + opacity: 0.45; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.errorText { + margin: 0; + font-size: 12px; + color: #ef4444; +} + .modalIn { animation: modalFadeIn 0.25s ease forwards; } diff --git a/apps/business/src/components/CategorySearchSelect.module.css b/apps/business/src/components/CategorySearchSelect.module.css index 4d39603..020646a 100644 --- a/apps/business/src/components/CategorySearchSelect.module.css +++ b/apps/business/src/components/CategorySearchSelect.module.css @@ -81,18 +81,16 @@ } .dropdown { - position: absolute; - top: calc(100% + 6px); - inset-inline: 0; - max-height: 260px; - overflow-y: auto; + position: static; + margin-top: 6px; + max-height: none; + overflow: visible; background: var(--elevated-surface); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--glass-border); border-radius: var(--radius-sm); box-shadow: var(--glass-shadow); - z-index: 30; list-style: none; padding: 6px; margin: 0; diff --git a/apps/business/src/components/CitySearchSelect.module.css b/apps/business/src/components/CitySearchSelect.module.css index 72ade7c..bf18b57 100644 --- a/apps/business/src/components/CitySearchSelect.module.css +++ b/apps/business/src/components/CitySearchSelect.module.css @@ -81,18 +81,16 @@ } .dropdown { - position: absolute; - top: calc(100% + 6px); - inset-inline: 0; - max-height: 220px; - overflow-y: auto; + position: static; + margin-top: 6px; + max-height: none; + overflow: visible; background: var(--elevated-surface); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--glass-border); border-radius: var(--radius-sm); box-shadow: var(--glass-shadow); - z-index: 20; list-style: none; padding: 6px; margin: 0; diff --git a/apps/business/src/components/MultiSelectDropdown.module.css b/apps/business/src/components/MultiSelectDropdown.module.css index 1fdfadf..674ad2e 100644 --- a/apps/business/src/components/MultiSelectDropdown.module.css +++ b/apps/business/src/components/MultiSelectDropdown.module.css @@ -64,15 +64,13 @@ } .dropdown { - position: absolute; - top: calc(100% + 4px); - left: 0; - right: 0; - z-index: 20; + position: static; + margin-top: 4px; + z-index: auto; display: flex; flex-direction: column; - max-height: 240px; - overflow: hidden; + max-height: none; + overflow: visible; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); diff --git a/apps/business/src/components/PageContent.module.css b/apps/business/src/components/PageContent.module.css index 8b1f580..d1f395a 100644 --- a/apps/business/src/components/PageContent.module.css +++ b/apps/business/src/components/PageContent.module.css @@ -108,6 +108,10 @@ margin-top: 24px; } +.span4 { + grid-column: span 4; +} + .col6 { grid-column: span 6; } @@ -121,6 +125,10 @@ grid-template-columns: repeat(4, 1fr); gap: 24px; } + + .span4 { + grid-column: span 2; + } } @media (max-width: 1400px) { @@ -133,6 +141,10 @@ .gridHome { grid-template-columns: repeat(3, 1fr); } + + .span4 { + grid-column: span 2; + } } @media (max-width: 900px) { @@ -141,6 +153,10 @@ .gridCols4 { grid-template-columns: repeat(2, 1fr); } + + .span4 { + grid-column: span 2; + } } @media (max-width: 768px) { @@ -173,7 +189,8 @@ gap: 16px; } + .span4, .col6 { - grid-column: span 12; + grid-column: span 1; } } diff --git a/apps/business/src/components/SearchableSelect.module.css b/apps/business/src/components/SearchableSelect.module.css index 68f0379..8ba1cab 100644 --- a/apps/business/src/components/SearchableSelect.module.css +++ b/apps/business/src/components/SearchableSelect.module.css @@ -69,31 +69,29 @@ } .dropdown { - position: absolute; - top: calc(100% + 6px); - left: 0; - right: 0; - max-height: 220px; - overflow-y: auto; + position: static; + margin-top: 6px; + max-height: none; + overflow: visible; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--glass-border); border-radius: var(--radius-sm); box-shadow: 0 8px 24px rgba(31, 38, 135, 0.12); - z-index: 10; list-style: none; padding: 6px; animation: dropdownIn 0.15s ease; } -/* Prefer for selects inside overflow-clipped modals — expands parent instead of clipping. */ +/* Alias — in-flow is the default so search results expand the parent modal. */ .dropdownInFlow { position: static; top: auto; left: auto; right: auto; - max-height: min(50vh, 400px); + max-height: none; + overflow: visible; margin-top: 6px; z-index: auto; } diff --git a/apps/business/src/components/SectionCard.tsx b/apps/business/src/components/SectionCard.tsx index bde173c..a385bd0 100644 --- a/apps/business/src/components/SectionCard.tsx +++ b/apps/business/src/components/SectionCard.tsx @@ -8,6 +8,7 @@ interface SectionCardProps { description: string linkText?: string href: string + className?: string /** Entity total shown beside the arrow; omit for sections without a count (e.g. settings). */ count?: number | null countLabel?: string @@ -18,6 +19,7 @@ export function SectionCard({ title, description, href, + className, count, countLabel, }: SectionCardProps) { @@ -27,7 +29,7 @@ export function SectionCard({ : null return ( - +
diff --git a/apps/business/src/components/Sidebar.tsx b/apps/business/src/components/Sidebar.tsx index 3df9385..6fc6604 100644 --- a/apps/business/src/components/Sidebar.tsx +++ b/apps/business/src/components/Sidebar.tsx @@ -212,7 +212,6 @@ export function Sidebar() { { labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' }, { labelKey: 'nav.website.faq', to: '/website/faq' }, { labelKey: 'nav.website.badges', to: '/website/badges' }, - { labelKey: 'nav.website.ePayment', to: '/website/e-payment' }, ], }, { type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' }, diff --git a/apps/business/src/components/StoreItemDiscountModal.tsx b/apps/business/src/components/StoreItemDiscountModal.tsx index 7a3e1d7..3e3b894 100644 --- a/apps/business/src/components/StoreItemDiscountModal.tsx +++ b/apps/business/src/components/StoreItemDiscountModal.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { X } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' +import { useT } from '../i18n/useT' import { ApiError } from '../lib/api' import { updateStoreItemDiscounts, @@ -15,6 +17,7 @@ import rowStyles from './CreateStoreItemsModal.module.css' interface StoreItemDiscountModalProps { open: boolean productTitle: string + productNameFa?: string items: StoreItem[] onClose: () => void onSaved?: (items: StoreItem[]) => void @@ -33,10 +36,15 @@ const ANIMATION_MS = 220 export function StoreItemDiscountModal({ open, productTitle, + productNameFa = '', items, onClose, onSaved, }: StoreItemDiscountModalProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' + const displayTitle = isFa ? productNameFa || productTitle : productTitle || productNameFa const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [rows, setRows] = useState([]) @@ -113,7 +121,7 @@ export function StoreItemDiscountModal({ if (err instanceof ApiError) { setError(err.message) } else { - setError('Unable to save discounts.') + setError(t('storeItems.discount.errorSave')) } } finally { setIsSubmitting(false) @@ -131,24 +139,26 @@ export function StoreItemDiscountModal({ role="dialog" aria-modal="true" aria-labelledby="store-discount-title" + lang={isFa ? 'fa' : 'en'} + dir={isFa ? 'rtl' : 'ltr'} >

- Set Discounts + {t('storeItems.discount.title')}

-

{productTitle}

+

{displayTitle}

-
void handleSubmit(e)}>
- Variant - Regular price - Discounted price (IRT) + {t('storeItems.discount.variant')} + {t('storeItems.discount.regularPrice')} + {t('storeItems.discount.discountedPrice')}
@@ -172,7 +182,7 @@ export function StoreItemDiscountModal({ ), ) } - placeholder="No discount" + placeholder={t('storeItems.discount.placeholder')} disabled={isSubmitting} />
@@ -188,10 +198,10 @@ export function StoreItemDiscountModal({ onClick={onClose} disabled={isSubmitting} > - Cancel + {t('products.form.cancel')}
diff --git a/apps/business/src/components/StoreItemMultiSearchSelect.module.css b/apps/business/src/components/StoreItemMultiSearchSelect.module.css index 87772ab..04b79b4 100644 --- a/apps/business/src/components/StoreItemMultiSearchSelect.module.css +++ b/apps/business/src/components/StoreItemMultiSearchSelect.module.css @@ -4,10 +4,6 @@ gap: 12px; } -.dropdown { - max-height: 280px; -} - .option { display: flex; align-items: center; diff --git a/apps/business/src/components/StoreItemMultiSearchSelect.tsx b/apps/business/src/components/StoreItemMultiSearchSelect.tsx index c62e22f..1b8a468 100644 --- a/apps/business/src/components/StoreItemMultiSearchSelect.tsx +++ b/apps/business/src/components/StoreItemMultiSearchSelect.tsx @@ -125,7 +125,7 @@ export function StoreItemMultiSearchSelect({ {showDropdown && ( -
    +
      {filtered.length === 0 ? (
    • {options.length === 0 @@ -164,7 +164,7 @@ export function StoreItemMultiSearchSelect({ )} {open && query.trim().length > 0 && query.trim().length < minSearchLength && ( -
        +
        • {t('storeItems.search.minChars', { count: minSearchLength })}
        • diff --git a/apps/business/src/components/StoreSpecialModal.tsx b/apps/business/src/components/StoreSpecialModal.tsx index d58c5c4..d526493 100644 --- a/apps/business/src/components/StoreSpecialModal.tsx +++ b/apps/business/src/components/StoreSpecialModal.tsx @@ -19,11 +19,45 @@ interface StoreSpecialModalProps { title?: string submitLabel?: string isSubmitting?: boolean + error?: string } const ANIMATION_MS = 220 const KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/ +function slugifyGroupKey(raw: string): string { + const slug = raw + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + if (!slug || !/^[a-z]/.test(slug)) return '' + return KEY_PATTERN.test(slug) ? slug : '' +} + +function hasNonAscii(value: string): boolean { + return /[^\u0000-\u007F]/.test(value) +} + +function resolveGroupFields(keyRaw: string, titleRaw: string): StoreSpecialModalValues | null { + const key = keyRaw.trim() + const title = titleRaw.trim() + const keySlug = slugifyGroupKey(key) + const titleSlug = slugifyGroupKey(title) + const keyLooksLikeTitle = key.length > 0 && !keySlug && hasNonAscii(key) + + if (keySlug && title) return { key: keySlug, title } + if (keyLooksLikeTitle && titleSlug) return { key: titleSlug, title: key } + if (!key && titleSlug) return { key: titleSlug, title } + if (title && !keySlug && !titleSlug) { + return { key: `special-${Date.now().toString(36)}`, title } + } + if (keyLooksLikeTitle && !title) { + return { key: `special-${Date.now().toString(36)}`, title: key } + } + return null +} + export function StoreSpecialModal({ open, onClose, @@ -33,6 +67,7 @@ export function StoreSpecialModal({ title, submitLabel, isSubmitting = false, + error, }: StoreSpecialModalProps) { const t = useT() const { locale } = useLocale() @@ -42,6 +77,7 @@ export function StoreSpecialModal({ const [closing, setClosing] = useState(false) const [groupKey, setGroupKey] = useState(initialKey) const [groupTitle, setGroupTitle] = useState(initialTitle) + const [keyTouched, setKeyTouched] = useState(Boolean(initialKey)) useEffect(() => { if (open) { @@ -49,6 +85,7 @@ export function StoreSpecialModal({ setClosing(false) setGroupKey(initialKey) setGroupTitle(initialTitle) + setKeyTouched(Boolean(initialKey)) } else if (mounted) { setClosing(true) const timer = setTimeout(() => { @@ -70,15 +107,22 @@ export function StoreSpecialModal({ if (!mounted) return null - const trimmedKey = groupKey.trim() - const trimmedTitle = groupTitle.trim() - const keyValid = KEY_PATTERN.test(trimmedKey) - const canSubmit = keyValid && trimmedTitle.length > 0 + const resolved = resolveGroupFields(groupKey, groupTitle) + const canSubmit = resolved !== null + const keySlug = slugifyGroupKey(groupKey) + const showKeyError = groupKey.trim().length > 0 && !keySlug && !hasNonAscii(groupKey.trim()) + + function handleTitleChange(value: string) { + setGroupTitle(value) + if (!keyTouched) { + setGroupKey(slugifyGroupKey(value)) + } + } function handleSubmit(e: React.FormEvent) { e.preventDefault() - if (!canSubmit) return - onSubmit({ key: trimmedKey, title: trimmedTitle }) + if (!resolved) return + onSubmit(resolved) } return createPortal( @@ -110,33 +154,47 @@ export function StoreSpecialModal({
          +
          + + handleTitleChange(e.target.value)} + placeholder={t('website.groupValuePlaceholder')} + autoFocus + disabled={isSubmitting} + dir={isFa ? 'rtl' : 'ltr'} + /> +
          +
          setGroupKey(e.target.value)} + onChange={(e) => { + setKeyTouched(true) + setGroupKey(e.target.value) + }} placeholder={t('website.groupKeyPlaceholder')} - autoFocus disabled={isSubmitting} dir="ltr" autoComplete="off" spellCheck={false} />

          {t('website.groupKeyHint')}

          + {showKeyError ? ( +

          + {t('website.groupKeyError')} +

          + ) : null}
          -
          - - setGroupTitle(e.target.value)} - placeholder={t('website.groupValuePlaceholder')} - disabled={isSubmitting} - dir={isFa ? 'rtl' : 'ltr'} - /> -
          + {error ? ( +

          + {error} +

          + ) : null}
          +
          + + +
          {COMING_SOON_GATEWAYS.map((gateway) => (
          @@ -354,6 +509,6 @@ export function WebsiteEPaymentPage() {
          )} - + ) } diff --git a/apps/business/src/pages/StoreItemsPage.tsx b/apps/business/src/pages/StoreItemsPage.tsx index 5cd9d8a..5f06479 100644 --- a/apps/business/src/pages/StoreItemsPage.tsx +++ b/apps/business/src/pages/StoreItemsPage.tsx @@ -468,6 +468,7 @@ function StoreItemsPageContent() { setDiscountTarget(null)} onSaved={mergeUpdatedItems} diff --git a/apps/business/src/pages/StoreSettingsPage.tsx b/apps/business/src/pages/StoreSettingsPage.tsx index e4ba1eb..e6beac1 100644 --- a/apps/business/src/pages/StoreSettingsPage.tsx +++ b/apps/business/src/pages/StoreSettingsPage.tsx @@ -44,7 +44,11 @@ const DEFAULT_STORE_SETTINGS: StoreSettings = { sep: { enabled: false }, snappay: { enabled: false }, digipay: { enabled: false }, - zarinpal: { enabled: false }, + zarinpal: { + enabled: false, + merchantId: '', + sandbox: false, + }, }, }, } diff --git a/apps/business/src/pages/StoreSpecialsPage.tsx b/apps/business/src/pages/StoreSpecialsPage.tsx index d4b0037..8311546 100644 --- a/apps/business/src/pages/StoreSpecialsPage.tsx +++ b/apps/business/src/pages/StoreSpecialsPage.tsx @@ -158,11 +158,9 @@ export function StoreSpecialsPage() { setCreateOpen(false) showToast(t('website.specialItems.toastCreated'), 'success') } catch (err) { - if (err instanceof ApiError) { - setError(err.message) - } else { - setError(t('website.specialItems.errorCreate')) - } + const message = err instanceof ApiError ? err.message : t('website.specialItems.errorCreate') + setError(message) + showToast(message, 'error') } finally { setIsSaving(false) } @@ -183,11 +181,9 @@ export function StoreSpecialsPage() { setEditSpecialTarget(null) showToast(t('website.specialItems.toastUpdated'), 'success') } catch (err) { - if (err instanceof ApiError) { - setError(err.message) - } else { - setError(t('website.specialItems.errorUpdate')) - } + const message = err instanceof ApiError ? err.message : t('website.specialItems.errorUpdate') + setError(message) + showToast(message, 'error') } finally { setIsSaving(false) } @@ -362,7 +358,10 @@ export function StoreSpecialsPage() { + {onlinePaymentsEnabled ? ( + + ) : null} - {paymentType === 'e_payment_gate' && ( + {onlinePaymentsEnabled && paymentType === 'e_payment_gate' && (
          - {ONLINE_GATEWAYS.map((gateway) => { + {onlineGateways.map((gateway) => { const active = gatewayType === gateway.id return ( ) })} diff --git a/apps/customer/src/services/cartService.ts b/apps/customer/src/services/cartService.ts index 8ea3d54..a52c740 100644 --- a/apps/customer/src/services/cartService.ts +++ b/apps/customer/src/services/cartService.ts @@ -83,6 +83,18 @@ export interface CheckoutResponse { payment?: CheckoutPaymentResult } +export interface PublicPaymentGateway { + id: OnlineGatewayType + label: string + labelFa: string +} + +export interface PublicPaymentMethods { + enabled: boolean + defaultGateway: OnlineGatewayType | null + gateways: PublicPaymentGateway[] +} + function cartPath(suffix = '') { const businessId = getActiveBusinessId() if (!businessId) { @@ -91,6 +103,14 @@ function cartPath(suffix = '') { return `/businesses/${businessId}/cart${suffix}` } +function paymentsMethodsPath() { + const businessId = getActiveBusinessId() + if (!businessId) { + throw new Error('No active business selected. Please sign in again.') + } + return `/businesses/${businessId}/payments/methods` +} + export async function getCart(signal?: AbortSignal) { return apiRequest<{ cart: Cart }>(cartPath(), { auth: true, signal }) } @@ -126,6 +146,13 @@ export async function checkoutCart(input: CheckoutInput) { }) } +export async function getPaymentMethods(signal?: AbortSignal) { + return apiRequest(paymentsMethodsPath(), { + auth: false, + signal, + }) +} + /** Submit a gateway redirect (POST form or GET navigation). */ export function submitPaymentRedirect(redirect: PaymentRedirect) { if (redirect.method === 'GET') { diff --git a/apps/super-admin/src/components/Header.module.css b/apps/super-admin/src/components/Header.module.css index 466d97d..6fe8c05 100644 --- a/apps/super-admin/src/components/Header.module.css +++ b/apps/super-admin/src/components/Header.module.css @@ -82,7 +82,7 @@ display: flex; align-items: center; gap: 12px; - padding: 6px 12px 6px 6px; + padding: 8px 16px; border-radius: 50px; background: rgba(255, 255, 255, 0.45); backdrop-filter: blur(12px); @@ -94,6 +94,7 @@ .profileInfo { display: flex; flex-direction: column; + padding: 0 4px; } .name { diff --git a/apps/super-admin/src/index.css b/apps/super-admin/src/index.css index 14c8957..80ee763 100644 --- a/apps/super-admin/src/index.css +++ b/apps/super-admin/src/index.css @@ -164,3 +164,8 @@ textarea::placeholder { font-family: var(--font-ui); font-weight: 400; } + +body:has([aria-modal='true']) #root { + filter: blur(var(--modal-overlay-blur, 16px)); + pointer-events: none; +} diff --git a/apps/super-admin/src/pages/UsersPage.module.css b/apps/super-admin/src/pages/UsersPage.module.css index 2f7844d..f6844a5 100644 --- a/apps/super-admin/src/pages/UsersPage.module.css +++ b/apps/super-admin/src/pages/UsersPage.module.css @@ -1,6 +1,11 @@ +.colName, +.colRole { + padding-inline: 22px; +} + .roleBadge { display: inline-block; - padding: 3px 8px; + padding: 3px 12px; border-radius: 999px; font-size: 11px; font-weight: 600; @@ -9,6 +14,23 @@ border: 1px solid rgba(var(--primary-rgb) / 0.18); } +.shieldSuperAdmin { + color: #0f172a; +} + +.shieldSuperAdmin:hover:not(:disabled) { + color: #0f172a; + background: rgba(15, 23, 42, 0.08); +} + +.shieldOwner { + color: var(--primary); +} + +.shieldOwner:hover:not(:disabled) { + color: var(--primary); +} + .inactiveRow { opacity: 0.55; } diff --git a/apps/super-admin/src/pages/UsersPage.tsx b/apps/super-admin/src/pages/UsersPage.tsx index be6a5ce..b579a8c 100644 --- a/apps/super-admin/src/pages/UsersPage.tsx +++ b/apps/super-admin/src/pages/UsersPage.tsx @@ -41,6 +41,18 @@ function displayName(user: UserListItem) { return name || '—' } +function isSuperAdminUser(user: UserListItem) { + return user.roleSlug === 'super_admin' || /\bsuper admin\b/i.test(user.roles ?? '') +} + +function isBusinessOwnerUser(user: UserListItem) { + return ( + user.roleSlug === 'business_owner' || + Boolean(user.isBusinessOwner) || + /\bbusiness owner\b/i.test(user.roles ?? '') + ) +} + const TEAM_ROLE_ORDER = ['admin', 'editor', 'viewer'] as const const TEAM_ROLE_LABELS: Record = { @@ -644,9 +656,9 @@ export function UsersPage() { - + - + @@ -672,12 +684,12 @@ export function UsersPage() { {!loading && data?.items?.map((user) => ( - -
          NameName Cell numberRoleRole Business Date joined Actions
          +
          {displayName(user)}
          {!user.isActive &&
          Inactive
          }
          {formatCellForDisplay(user.cellNumber)} + {(() => { const label = businessFilter ? formatBusinessUserRoleLabel(user, teamRoles) @@ -697,7 +709,13 @@ export function UsersPage() {