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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
9a59bc8f65
commit
e79211e3cd
@@ -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() {
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="business-profile" element={<BusinessProfilePage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="products" element={<ProductsPage />} />
|
||||
<Route path="products/categories" element={<CategoriesPage />} />
|
||||
<Route path="products/brands" element={<BrandsPage />} />
|
||||
@@ -155,7 +156,7 @@ function App() {
|
||||
<Route path="website/subscriptions" element={<WebsiteSubscriptionsPage />} />
|
||||
<Route path="website/faq" element={<WebsiteFaqPage />} />
|
||||
<Route path="website/badges" element={<WebsiteBadgesPage />} />
|
||||
<Route path="website/e-payment" element={<WebsiteEPaymentPage />} />
|
||||
<Route path="website/e-payment" element={<Navigate to="/settings" replace />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Link to={href} className={styles.card}>
|
||||
<Link to={href} className={className ? `${styles.card} ${className}` : styles.card}>
|
||||
<div className={styles.iconWrap}>
|
||||
<Icon size={24} strokeWidth={1.75} />
|
||||
</div>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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<DiscountDraftRow[]>([])
|
||||
@@ -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'}
|
||||
>
|
||||
<div className={modalStyles.header}>
|
||||
<div>
|
||||
<h3 id="store-discount-title" className={modalStyles.title}>
|
||||
Set Discounts
|
||||
{t('storeItems.discount.title')}
|
||||
</h3>
|
||||
<p className={modalStyles.subtitle}>{productTitle}</p>
|
||||
<p className={modalStyles.subtitle}>{displayTitle}</p>
|
||||
</div>
|
||||
<button className={modalStyles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<button className={modalStyles.closeBtn} onClick={onClose} aria-label={t('common.close')}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form className={modalStyles.body} onSubmit={(e) => void handleSubmit(e)}>
|
||||
<div className={rowStyles.discountHeader}>
|
||||
<span>Variant</span>
|
||||
<span>Regular price</span>
|
||||
<span>Discounted price (IRT)</span>
|
||||
<span>{t('storeItems.discount.variant')}</span>
|
||||
<span>{t('storeItems.discount.regularPrice')}</span>
|
||||
<span>{t('storeItems.discount.discountedPrice')}</span>
|
||||
</div>
|
||||
|
||||
<div className={rowStyles.discountRows}>
|
||||
@@ -172,7 +182,7 @@ export function StoreItemDiscountModal({
|
||||
),
|
||||
)
|
||||
}
|
||||
placeholder="No discount"
|
||||
placeholder={t('storeItems.discount.placeholder')}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
@@ -188,10 +198,10 @@ export function StoreItemDiscountModal({
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t('products.form.cancel')}
|
||||
</button>
|
||||
<button type="submit" className={modalStyles.submitBtn} disabled={!canSubmit}>
|
||||
{isSubmitting ? 'Saving…' : 'Save discounts'}
|
||||
{isSubmitting ? t('storeItems.discount.saving') : t('storeItems.discount.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
max-height: 280px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -125,7 +125,7 @@ export function StoreItemMultiSearchSelect({
|
||||
</div>
|
||||
|
||||
{showDropdown && (
|
||||
<ul className={`${selectStyles.dropdown} ${styles.dropdown}`} role="listbox">
|
||||
<ul className={`${selectStyles.dropdown} ${selectStyles.dropdownInFlow}`} role="listbox">
|
||||
{filtered.length === 0 ? (
|
||||
<li className={selectStyles.noResults}>
|
||||
{options.length === 0
|
||||
@@ -164,7 +164,7 @@ export function StoreItemMultiSearchSelect({
|
||||
)}
|
||||
|
||||
{open && query.trim().length > 0 && query.trim().length < minSearchLength && (
|
||||
<ul className={selectStyles.dropdown}>
|
||||
<ul className={`${selectStyles.dropdown} ${selectStyles.dropdownInFlow}`}>
|
||||
<li className={selectStyles.noResults}>
|
||||
{t('storeItems.search.minChars', { count: minSearchLength })}
|
||||
</li>
|
||||
|
||||
@@ -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<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
if (!canSubmit) return
|
||||
onSubmit({ key: trimmedKey, title: trimmedTitle })
|
||||
if (!resolved) return
|
||||
onSubmit(resolved)
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
@@ -110,33 +154,47 @@ export function StoreSpecialModal({
|
||||
</div>
|
||||
|
||||
<form ref={formRef} className={styles.form} onSubmit={handleSubmit}>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="special-group-title">{t('website.groupValue')}</label>
|
||||
<input
|
||||
id="special-group-title"
|
||||
value={groupTitle}
|
||||
onChange={(e) => handleTitleChange(e.target.value)}
|
||||
placeholder={t('website.groupValuePlaceholder')}
|
||||
autoFocus
|
||||
disabled={isSubmitting}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="special-group-key">{t('website.groupKey')}</label>
|
||||
<input
|
||||
id="special-group-key"
|
||||
value={groupKey}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
<p className={styles.hint}>{t('website.groupKeyHint')}</p>
|
||||
{showKeyError ? (
|
||||
<p className={styles.errorText} role="alert">
|
||||
{t('website.groupKeyError')}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="special-group-title">{t('website.groupValue')}</label>
|
||||
<input
|
||||
id="special-group-title"
|
||||
value={groupTitle}
|
||||
onChange={(e) => setGroupTitle(e.target.value)}
|
||||
placeholder={t('website.groupValuePlaceholder')}
|
||||
disabled={isSubmitting}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className={styles.errorText} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--modal-overlay-bg);
|
||||
backdrop-filter: blur(var(--modal-overlay-blur));
|
||||
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
|
||||
backdrop-filter: blur(var(--modal-overlay-blur, 16px));
|
||||
-webkit-backdrop-filter: blur(var(--modal-overlay-blur, 16px));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -11,6 +11,14 @@
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.overlayIn {
|
||||
animation: overlayFadeIn 0.2s ease forwards;
|
||||
}
|
||||
|
||||
.overlayOut {
|
||||
animation: overlayFadeOut 0.18s ease forwards;
|
||||
}
|
||||
|
||||
.overlayNested {
|
||||
z-index: 1010;
|
||||
}
|
||||
@@ -417,6 +425,16 @@
|
||||
color: #ef4444;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@keyframes overlayFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes overlayFadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from { opacity: 0; transform: translateY(12px) scale(0.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
|
||||
@@ -218,7 +218,7 @@ const en = {
|
||||
'home.card.portfolios.link': 'View portfolios',
|
||||
'home.card.portfolios.count': 'portfolios',
|
||||
'home.card.website.title': 'Website',
|
||||
'home.card.website.desc': 'Manage contact forms, FAQ, badges, subscriptions, and e-payment.',
|
||||
'home.card.website.desc': 'Manage contact forms, FAQ, badges, and subscriptions.',
|
||||
'home.card.website.link': 'View website',
|
||||
'home.card.finance.title': 'Finance Management',
|
||||
'home.card.finance.desc': 'Manage invoices and financial transactions.',
|
||||
@@ -728,6 +728,14 @@ const en = {
|
||||
'storeItems.search.noProducts': 'No products available.',
|
||||
'storeItems.search.noMatch': 'No matching products.',
|
||||
'storeItems.search.minChars': 'Type at least {count} characters to search',
|
||||
'storeItems.discount.title': 'Set Discounts',
|
||||
'storeItems.discount.variant': 'Variant',
|
||||
'storeItems.discount.regularPrice': 'Regular price',
|
||||
'storeItems.discount.discountedPrice': 'Discounted price (IRT)',
|
||||
'storeItems.discount.placeholder': 'No discount',
|
||||
'storeItems.discount.save': 'Save discounts',
|
||||
'storeItems.discount.saving': 'Saving…',
|
||||
'storeItems.discount.errorSave': 'Unable to save discounts.',
|
||||
|
||||
'orders.title': 'Orders',
|
||||
'orders.subtitle':
|
||||
@@ -1263,8 +1271,7 @@ const en = {
|
||||
'website.card.faq.link': 'Manage FAQ',
|
||||
'website.card.badges.desc': 'Show trust badges, certifications, and highlights on your website.',
|
||||
'website.card.badges.link': 'Manage badges',
|
||||
'website.card.ePayment.desc': 'Configure online payment gateways and checkout payment options.',
|
||||
'website.card.ePayment.link': 'Manage e-payment',
|
||||
'settings.subtitle': 'Configure online payment gateways and checkout payment options.',
|
||||
|
||||
'website.scrollLeft': 'Scroll left',
|
||||
'website.scrollRight': 'Scroll right',
|
||||
@@ -1281,6 +1288,8 @@ const en = {
|
||||
'website.groupKeyPlaceholder': 'e.g. top-selling',
|
||||
'website.groupKeyHint':
|
||||
'English only — letters, numbers, hyphens, underscores. Used by websites.',
|
||||
'website.groupKeyError':
|
||||
'Use English letters, numbers, hyphens, or underscores only — e.g. special-sale.',
|
||||
'website.groupValue': 'Display title',
|
||||
'website.groupValuePlaceholder': 'Shown on the website in any language',
|
||||
'website.removeFromGroupTitle': 'Remove from Group',
|
||||
@@ -1461,10 +1470,10 @@ const en = {
|
||||
'website.ePayment.subtitle': 'Configure online payment methods for your website checkout.',
|
||||
'website.ePayment.master': 'Enable online payments',
|
||||
'website.ePayment.masterDesc':
|
||||
'When enabled, customers can pay through a configured gateway at website checkout.',
|
||||
'When a gateway is enabled and configured, customers can use it at website checkout or in the app.',
|
||||
'website.ePayment.gateway.mellat': 'Bank Mellat (Behpardakht)',
|
||||
'website.ePayment.gateway.mellatDesc':
|
||||
'First gateway. Enter terminal ID, username, and password from the Mellat merchant panel.',
|
||||
'Enter terminal ID, username, and password from the Mellat merchant panel.',
|
||||
'website.ePayment.gateway.sep': 'SEP (Saman Kish)',
|
||||
'website.ePayment.gateway.sepDesc': 'Saman electronic payment — coming soon.',
|
||||
'website.ePayment.gateway.snappay': 'Snapp Pay',
|
||||
@@ -1916,7 +1925,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'nav.website.sliders': 'اسلایدرها',
|
||||
'nav.website.specialCategories': 'دستههای ویژه',
|
||||
'nav.website.specialBrands': 'برندهای ویژه',
|
||||
'nav.website.specialItems': 'اقلام ویژه',
|
||||
'nav.website.specialItems': 'محصولات ویژه فروشگاه',
|
||||
'nav.website.contact': 'فرم تماس با ما',
|
||||
'nav.website.subscriptions': 'عضویتها',
|
||||
'nav.website.faq': 'سوالات متداول',
|
||||
@@ -2062,7 +2071,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'home.card.videos.link': 'مشاهده ویدیوها',
|
||||
'home.card.videos.count': 'ویدیو',
|
||||
'home.card.website.title': 'وبسایت',
|
||||
'home.card.website.desc': 'فرم تماس، سوالات متداول، نشانها، عضویتها و پرداخت الکترونیک.',
|
||||
'home.card.website.desc': 'فرم تماس، سوالات متداول، نشانها و عضویتها.',
|
||||
'home.card.website.link': 'مشاهده وبسایت',
|
||||
'home.card.finance.title': 'مدیریت مالی',
|
||||
'home.card.finance.desc': 'فاکتورها و تراکنشهای مالی را مدیریت کنید.',
|
||||
@@ -2570,6 +2579,14 @@ const fa: Record<MessageKey, string> = {
|
||||
'storeItems.search.noProducts': 'محصولی موجود نیست.',
|
||||
'storeItems.search.noMatch': 'محصولی یافت نشد.',
|
||||
'storeItems.search.minChars': 'حداقل {count} کاراکتر برای جستجو وارد کنید',
|
||||
'storeItems.discount.title': 'تنظیم تخفیف',
|
||||
'storeItems.discount.variant': 'تنوع',
|
||||
'storeItems.discount.regularPrice': 'قیمت عادی',
|
||||
'storeItems.discount.discountedPrice': 'قیمت با تخفیف (IRT)',
|
||||
'storeItems.discount.placeholder': 'بدون تخفیف',
|
||||
'storeItems.discount.save': 'ذخیره تخفیف',
|
||||
'storeItems.discount.saving': 'در حال ذخیره…',
|
||||
'storeItems.discount.errorSave': 'ذخیره تخفیف ممکن نشد.',
|
||||
|
||||
'orders.title': 'سفارشها',
|
||||
'orders.subtitle':
|
||||
@@ -3096,8 +3113,8 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.card.specialBrands.desc': 'برندهای شریک یا ویژه را در صفحه اصلی وبسایت نمایش دهید.',
|
||||
'website.card.specialBrands.link': 'مدیریت برندها',
|
||||
'website.card.specialItems.desc':
|
||||
'اقلام فروشگاه ویژه را در دستهها برای کاروسلهای وبسایت گردآوری کنید.',
|
||||
'website.card.specialItems.link': 'مدیریت اقلام ویژه',
|
||||
'محصولات ویژه فروشگاه را در دستهها برای کاروسلهای وبسایت گردآوری کنید.',
|
||||
'website.card.specialItems.link': 'مدیریت محصولات ویژه',
|
||||
'website.card.contact.desc': 'ارسالهای فرم تماس وبسایت را مشاهده کنید.',
|
||||
'website.card.contact.link': 'مشاهده ارسالها',
|
||||
'website.card.subscriptions.desc': 'عضویت خبرنامه و گزینههای اشتراک بازدیدکنندگان را مدیریت کنید.',
|
||||
@@ -3106,8 +3123,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.card.faq.link': 'مدیریت سوالات متداول',
|
||||
'website.card.badges.desc': 'نشانهای اعتماد، گواهیها و برجستهها را در وبسایت نمایش دهید.',
|
||||
'website.card.badges.link': 'مدیریت نشانها',
|
||||
'website.card.ePayment.desc': 'درگاههای پرداخت آنلاین و گزینههای تسویه حساب را پیکربندی کنید.',
|
||||
'website.card.ePayment.link': 'مدیریت پرداخت الکترونیک',
|
||||
'settings.subtitle': 'درگاههای پرداخت آنلاین و گزینههای تسویه حساب را پیکربندی کنید.',
|
||||
|
||||
'website.scrollLeft': 'اسکرول به چپ',
|
||||
'website.scrollRight': 'اسکرول به راست',
|
||||
@@ -3124,6 +3140,8 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.groupKeyPlaceholder': 'مثلاً: top-selling',
|
||||
'website.groupKeyHint':
|
||||
'فقط انگلیسی — حروف، اعداد، خط تیره و زیرخط. برای استفاده وبسایت.',
|
||||
'website.groupKeyError':
|
||||
'فقط حروف انگلیسی، اعداد، خط تیره یا زیرخط — مثلاً special-sale.',
|
||||
'website.groupValue': 'عنوان نمایشی',
|
||||
'website.groupValuePlaceholder': 'عنوانی که در وبسایت نمایش داده میشود (هر زبانی)',
|
||||
'website.removeFromGroupTitle': 'حذف از گروه',
|
||||
@@ -3217,40 +3235,40 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.specialBrands.pickErrorLoad': 'بارگذاری برندها ممکن نشد.',
|
||||
|
||||
'website.specialItems.subtitle':
|
||||
'اقلام فروشگاه ویژه را برای کاروسلهای وبسایت در دستهها گردآوری کنید.',
|
||||
'محصولات ویژه فروشگاه را برای کاروسلهای وبسایت در دستهها گردآوری کنید.',
|
||||
'website.specialItems.loading': 'در حال بارگذاری دستههای ویژه...',
|
||||
'website.specialItems.empty':
|
||||
'هنوز دسته ویژهای نیست. با دکمه + یکی بسازید، سپس اقلام فروشگاه را به هر کاروسل اضافه کنید.',
|
||||
'هنوز دسته ویژهای نیست. با دکمه + یکی بسازید، سپس محصولات فروشگاه را به هر کاروسل اضافه کنید.',
|
||||
'website.specialItems.errorLoad': 'بارگذاری دستههای ویژه ممکن نشد.',
|
||||
'website.specialItems.errorCreate': 'ایجاد دسته ویژه ممکن نشد.',
|
||||
'website.specialItems.errorUpdate': 'بهروزرسانی دسته ویژه ممکن نشد.',
|
||||
'website.specialItems.errorDelete': 'حذف دسته ویژه ممکن نشد.',
|
||||
'website.specialItems.errorAdd': 'افزودن اقلام فروشگاه ممکن نشد.',
|
||||
'website.specialItems.errorRemove': 'حذف قلم فروشگاه ممکن نشد.',
|
||||
'website.specialItems.errorAdd': 'افزودن محصولات فروشگاه ممکن نشد.',
|
||||
'website.specialItems.errorRemove': 'حذف محصول فروشگاه ممکن نشد.',
|
||||
'website.specialItems.toastCreated': 'دسته ویژه ایجاد شد.',
|
||||
'website.specialItems.toastUpdated': 'دسته ویژه بهروزرسانی شد.',
|
||||
'website.specialItems.toastDeleted': 'دسته ویژه حذف شد.',
|
||||
'website.specialItems.toastAdded': 'اقلام فروشگاه به دسته ویژه افزوده شد.',
|
||||
'website.specialItems.toastAdded': 'محصولات فروشگاه به دسته ویژه افزوده شد.',
|
||||
'website.specialItems.toastRemoved': 'از دسته ویژه حذف شد.',
|
||||
'website.specialItems.addFab': 'افزودن دسته ویژه',
|
||||
'website.specialItems.createTitle': 'افزودن دسته ویژه',
|
||||
'website.specialItems.editTitle': 'ویرایش دسته ویژه',
|
||||
'website.specialItems.deleteTitle': 'حذف دسته ویژه',
|
||||
'website.specialItems.deleteMessage':
|
||||
'«{title}» حذف شود؟ اقلام فروشگاه در کاتالوگ باقی میمانند.',
|
||||
'«{title}» حذف شود؟ محصولات فروشگاه در کاتالوگ باقی میمانند.',
|
||||
'website.specialItems.removeTitle': 'حذف از ویژه',
|
||||
'website.specialItems.removeMessage': '«{name}» از «{group}» حذف شود؟',
|
||||
'website.specialItems.editCategory': 'ویرایش دسته',
|
||||
'website.specialItems.deleteCategory': 'حذف دسته',
|
||||
'website.specialItems.scrollLeft': 'اسکرول به چپ',
|
||||
'website.specialItems.scrollRight': 'اسکرول به راست',
|
||||
'website.specialItems.addItemsTo': 'افزودن اقلام فروشگاه به {title}',
|
||||
'website.specialItems.addItemsTo': 'افزودن محصولات فروشگاه به {title}',
|
||||
'website.specialItems.removeFromSpecial': 'حذف از ویژه',
|
||||
'website.specialItems.pickSubtitle': 'اقلام فروشگاه را برای نمایش در این دسته انتخاب کنید.',
|
||||
'website.specialItems.pickLoading': 'در حال بارگذاری اقلام فروشگاه...',
|
||||
'website.specialItems.pickEmpty': 'همه اقلام فروشگاه قبلاً در این دسته هستند.',
|
||||
'website.specialItems.pickSearch': 'حداقل ۳ حرف برای جستجوی اقلام فروشگاه تایپ کنید',
|
||||
'website.specialItems.pickErrorLoad': 'بارگذاری اقلام فروشگاه ممکن نشد.',
|
||||
'website.specialItems.pickSubtitle': 'محصولات فروشگاه را برای نمایش در این دسته انتخاب کنید.',
|
||||
'website.specialItems.pickLoading': 'در حال بارگذاری محصولات فروشگاه...',
|
||||
'website.specialItems.pickEmpty': 'همه محصولات فروشگاه قبلاً در این دسته هستند.',
|
||||
'website.specialItems.pickSearch': 'حداقل ۳ حرف برای جستجوی محصولات فروشگاه تایپ کنید',
|
||||
'website.specialItems.pickErrorLoad': 'بارگذاری محصولات فروشگاه ممکن نشد.',
|
||||
'website.pick.addOneItem': 'افزودن ۱ قلم',
|
||||
'website.pick.addItems': 'افزودن {count} قلم',
|
||||
|
||||
@@ -3301,10 +3319,10 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.ePayment.subtitle': 'روشهای پرداخت آنلاین برای تسویه حساب وبسایت را پیکربندی کنید.',
|
||||
'website.ePayment.master': 'فعالسازی پرداخت آنلاین',
|
||||
'website.ePayment.masterDesc':
|
||||
'با فعال بودن این گزینه، مشتریان در تسویه حساب وبسایت میتوانند از درگاه پیکربندیشده پرداخت کنند.',
|
||||
'با فعال بودن هر درگاه و پیکربندی آن، مشتریان در تسویه حساب وبسایت و یا اپلیکیشن از آن درگاه استفاده کنند.',
|
||||
'website.ePayment.gateway.mellat': 'بانک ملت (بهپرداخت)',
|
||||
'website.ePayment.gateway.mellatDesc':
|
||||
'اولین درگاه. شناسه ترمینال، نام کاربری و رمز عبور را از پنل پذیرنده ملت وارد کنید.',
|
||||
'شناسه ترمینال، نام کاربری و رمز عبور را از پنل پذیرنده ملت وارد کنید.',
|
||||
'website.ePayment.gateway.sep': 'سپ (سامان کیش)',
|
||||
'website.ePayment.gateway.sepDesc': 'درگاه پرداخت سامان — بهزودی.',
|
||||
'website.ePayment.gateway.snappay': 'اسنپپی',
|
||||
@@ -3379,7 +3397,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'title.sliders': 'اسلایدرها',
|
||||
'title.specialCategories': 'دستههای ویژه',
|
||||
'title.specialBrands': 'برندهای ویژه',
|
||||
'title.specialItems': 'اقلام ویژه',
|
||||
'title.specialItems': 'محصولات ویژه فروشگاه',
|
||||
'title.contactForm': 'فرم تماس با ما',
|
||||
'title.subscriptions': 'عضویتها',
|
||||
'title.faq': 'سوالات متداول',
|
||||
@@ -3860,8 +3878,8 @@ export function getBusinessRouteTitleRules(locale: DashboardLocale): RouteTitleR
|
||||
{ match: '/website/subscriptions', labels: [t('title.website'), t('title.subscriptions')] },
|
||||
{ match: '/website/faq', labels: [t('title.website'), t('title.faq')] },
|
||||
{ match: '/website/badges', labels: [t('title.website'), t('title.badges')] },
|
||||
{ match: '/website/e-payment', labels: [t('title.website'), t('title.ePayment')] },
|
||||
{ match: '/website', labels: [t('title.website')] },
|
||||
{ match: '/settings', labels: [t('title.settings')] },
|
||||
{ match: '/', labels: [t('title.home')] },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ body::before {
|
||||
}
|
||||
}
|
||||
|
||||
/* Portaled dialogs sit on body; blur the app behind them. */
|
||||
body:has([aria-modal='true']) #root {
|
||||
filter: blur(var(--modal-overlay-blur, 16px));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -40,6 +40,8 @@ const sections: {
|
||||
countLabelKey?: BusinessMessageKey
|
||||
href: string
|
||||
countKey?: CountKey
|
||||
/** Double-width tile (4 of 6 home columns). */
|
||||
wide?: boolean
|
||||
/** When set, card is shown only if this optional module is enabled. */
|
||||
moduleId?: BusinessModuleId
|
||||
/** When set, card is shown if any listed module is enabled. */
|
||||
@@ -63,6 +65,7 @@ const sections: {
|
||||
countLabelKey: 'home.card.store.count',
|
||||
href: '/store',
|
||||
countKey: 'store',
|
||||
wide: true,
|
||||
moduleId: 'store',
|
||||
},
|
||||
{
|
||||
@@ -185,6 +188,7 @@ export function HomePage() {
|
||||
description={t(section.descKey)}
|
||||
linkText={t(section.linkKey)}
|
||||
href={section.href}
|
||||
className={section.wide ? styles.span4 : undefined}
|
||||
count={section.countKey ? counts[section.countKey] : undefined}
|
||||
countLabel={section.countLabelKey ? t(section.countLabelKey) : undefined}
|
||||
/>
|
||||
|
||||
+7
-2
@@ -42,7 +42,7 @@
|
||||
|
||||
.credentials {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
@@ -50,10 +50,11 @@
|
||||
}
|
||||
|
||||
.field {
|
||||
grid-column: span 3;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field:last-of-type {
|
||||
.fullRow {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@ -142,6 +143,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.gatewayHeader {
|
||||
flex-direction: column;
|
||||
}
|
||||
+189
-34
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { WebsiteSectionShell } from '../components/WebsiteSectionShell'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { PageTitle } from '../components/PageTitle'
|
||||
import { Switch } from '../components/Switch'
|
||||
import { Tooltip } from '../components/Tooltip'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
@@ -14,12 +15,13 @@ import {
|
||||
type EPaymentSettings,
|
||||
type PaymentGatewayId,
|
||||
} from '../services/settingsService'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import sharedStyles from './ProductSettingsPage.module.css'
|
||||
import fieldStyles from './AddNewProductPage.module.css'
|
||||
import styles from './WebsiteEPaymentPage.module.css'
|
||||
import styles from './SettingsPage.module.css'
|
||||
|
||||
const COMING_SOON_GATEWAYS: {
|
||||
id: Exclude<PaymentGatewayId, 'mellat'>
|
||||
id: Exclude<PaymentGatewayId, 'mellat' | 'zarinpal'>
|
||||
nameKey: BusinessMessageKey
|
||||
descKey: BusinessMessageKey
|
||||
}[] = [
|
||||
@@ -38,14 +40,25 @@ const COMING_SOON_GATEWAYS: {
|
||||
nameKey: 'website.ePayment.gateway.digipay',
|
||||
descKey: 'website.ePayment.gateway.digipayDesc',
|
||||
},
|
||||
{
|
||||
id: 'zarinpal',
|
||||
nameKey: 'website.ePayment.gateway.zarinpal',
|
||||
descKey: 'website.ePayment.gateway.zarinpalDesc',
|
||||
},
|
||||
]
|
||||
|
||||
export function WebsiteEPaymentPage() {
|
||||
function nextDefaultGateway(
|
||||
current: EPaymentSettings,
|
||||
gatewayId: PaymentGatewayId,
|
||||
enabled: boolean,
|
||||
): PaymentGatewayId | null {
|
||||
if (enabled && !current.defaultGateway) return gatewayId
|
||||
if (!enabled && current.defaultGateway === gatewayId) {
|
||||
if (gatewayId !== 'mellat' && current.gateways.mellat.enabled) return 'mellat'
|
||||
if (gatewayId !== 'zarinpal' && current.gateways.zarinpal.enabled) {
|
||||
return 'zarinpal'
|
||||
}
|
||||
return null
|
||||
}
|
||||
return current.defaultGateway
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const t = useT()
|
||||
const { showToast } = useToast()
|
||||
const [ePayment, setEPayment] = useState<EPaymentSettings>(DEFAULT_EPAYMENT_SETTINGS)
|
||||
@@ -98,6 +111,10 @@ export function WebsiteEPaymentPage() {
|
||||
...next.gateways.mellat,
|
||||
password: password ?? '',
|
||||
},
|
||||
zarinpal: {
|
||||
...next.gateways.zarinpal,
|
||||
merchantId: next.gateways.zarinpal.merchantId.trim(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -121,7 +138,12 @@ export function WebsiteEPaymentPage() {
|
||||
...ePayment,
|
||||
enabled: checked,
|
||||
defaultGateway: checked
|
||||
? ePayment.defaultGateway ?? 'mellat'
|
||||
? ePayment.defaultGateway ??
|
||||
(ePayment.gateways.mellat.enabled
|
||||
? 'mellat'
|
||||
: ePayment.gateways.zarinpal.enabled
|
||||
? 'zarinpal'
|
||||
: 'mellat')
|
||||
: ePayment.defaultGateway,
|
||||
})
|
||||
}
|
||||
@@ -133,8 +155,18 @@ export function WebsiteEPaymentPage() {
|
||||
...ePayment.gateways,
|
||||
mellat: { ...ePayment.gateways.mellat, enabled: checked },
|
||||
},
|
||||
defaultGateway:
|
||||
checked && !ePayment.defaultGateway ? 'mellat' : ePayment.defaultGateway,
|
||||
defaultGateway: nextDefaultGateway(ePayment, 'mellat', checked),
|
||||
})
|
||||
}
|
||||
|
||||
async function handleZarinpalToggle(checked: boolean) {
|
||||
await patchEPayment('zarinpal-enabled', {
|
||||
...ePayment,
|
||||
gateways: {
|
||||
...ePayment.gateways,
|
||||
zarinpal: { ...ePayment.gateways.zarinpal, enabled: checked },
|
||||
},
|
||||
defaultGateway: nextDefaultGateway(ePayment, 'zarinpal', checked),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,36 +206,70 @@ export function WebsiteEPaymentPage() {
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSaveZarinpalCredentials(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
const merchantId = ePayment.gateways.zarinpal.merchantId.trim()
|
||||
|
||||
if (ePayment.gateways.zarinpal.enabled && !merchantId) {
|
||||
setError(t('website.ePayment.error.zarinpalMerchant'))
|
||||
return
|
||||
}
|
||||
|
||||
await patchEPayment('zarinpal-credentials', {
|
||||
...ePayment,
|
||||
gateways: {
|
||||
...ePayment.gateways,
|
||||
zarinpal: {
|
||||
...ePayment.gateways.zarinpal,
|
||||
merchantId,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<WebsiteSectionShell
|
||||
sectionLabel="E-Payment"
|
||||
title={t('title.ePayment')}
|
||||
subtitle={t('website.ePayment.subtitle')}
|
||||
>
|
||||
<main className={pageStyles.content}>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Settings' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className={pageStyles.pageHeader}>
|
||||
<div>
|
||||
<PageTitle en="SETTINGS">{t('title.settings')}</PageTitle>
|
||||
<p className={pageStyles.pageSubtitle}>{t('settings.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <div className={sharedStyles.alertError}>{error}</div> : null}
|
||||
|
||||
{isLoading ? (
|
||||
<p className={sharedStyles.status}>{t('productSettings.loading')}</p>
|
||||
) : (
|
||||
<div className={styles.stack}>
|
||||
<div className={sharedStyles.list}>
|
||||
<div className={sharedStyles.row}>
|
||||
<div className={sharedStyles.rowText}>
|
||||
<label className={sharedStyles.rowLabel} htmlFor="epayment-master">
|
||||
{t('website.ePayment.master')}
|
||||
</label>
|
||||
<p className={sharedStyles.rowDescription}>
|
||||
{t('website.ePayment.masterDesc')}
|
||||
</p>
|
||||
<section className={sharedStyles.panel}>
|
||||
<h3 className={sharedStyles.sectionTitle}>{t('title.ePayment')}</h3>
|
||||
<div className={sharedStyles.list}>
|
||||
<div className={sharedStyles.row}>
|
||||
<div className={sharedStyles.rowText}>
|
||||
<label className={sharedStyles.rowLabel} htmlFor="epayment-master">
|
||||
{t('website.ePayment.master')}
|
||||
</label>
|
||||
<p className={sharedStyles.rowDescription}>
|
||||
{t('website.ePayment.masterDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="epayment-master"
|
||||
checked={ePayment.enabled}
|
||||
disabled={savingKey !== null}
|
||||
onChange={(checked) => void handleMasterToggle(checked)}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
id="epayment-master"
|
||||
checked={ePayment.enabled}
|
||||
disabled={savingKey !== null}
|
||||
onChange={(checked) => void handleMasterToggle(checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.gatewayCard} data-card-hover>
|
||||
<div className={styles.gatewayHeader}>
|
||||
@@ -339,6 +405,95 @@ export function WebsiteEPaymentPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className={styles.gatewayCard} data-card-hover>
|
||||
<div className={styles.gatewayHeader}>
|
||||
<div>
|
||||
<h3 className={styles.gatewayTitle}>
|
||||
{t('website.ePayment.gateway.zarinpal')}
|
||||
</h3>
|
||||
<p className={styles.gatewayDesc}>
|
||||
{t('website.ePayment.gateway.zarinpalDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="epayment-zarinpal"
|
||||
checked={ePayment.gateways.zarinpal.enabled}
|
||||
disabled={savingKey !== null || !ePayment.enabled}
|
||||
onChange={(checked) => void handleZarinpalToggle(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form className={styles.credentials} onSubmit={handleSaveZarinpalCredentials}>
|
||||
<div className={`${fieldStyles.field} ${styles.field}`}>
|
||||
<label htmlFor="zarinpal-merchant">
|
||||
{t('website.ePayment.merchantId')}
|
||||
</label>
|
||||
<input
|
||||
id="zarinpal-merchant"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
dir="ltr"
|
||||
spellCheck={false}
|
||||
placeholder={t('website.ePayment.merchantIdPlaceholder')}
|
||||
value={ePayment.gateways.zarinpal.merchantId}
|
||||
disabled={savingKey !== null}
|
||||
onChange={(e) =>
|
||||
setEPayment((current) => ({
|
||||
...current,
|
||||
gateways: {
|
||||
...current.gateways,
|
||||
zarinpal: {
|
||||
...current.gateways.zarinpal,
|
||||
merchantId: e.target.value,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${sharedStyles.row} ${styles.fullRow}`}>
|
||||
<div className={sharedStyles.rowText}>
|
||||
<label className={sharedStyles.rowLabel} htmlFor="zarinpal-sandbox">
|
||||
{t('website.ePayment.sandbox')}
|
||||
</label>
|
||||
<p className={sharedStyles.rowDescription}>
|
||||
{t('website.ePayment.sandboxDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="zarinpal-sandbox"
|
||||
checked={ePayment.gateways.zarinpal.sandbox}
|
||||
disabled={savingKey !== null}
|
||||
onChange={(checked) =>
|
||||
setEPayment((current) => ({
|
||||
...current,
|
||||
gateways: {
|
||||
...current.gateways,
|
||||
zarinpal: {
|
||||
...current.gateways.zarinpal,
|
||||
sandbox: checked,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.saveBtn}
|
||||
disabled={savingKey !== null}
|
||||
>
|
||||
{savingKey === 'zarinpal-credentials'
|
||||
? t('storeSettings.saving')
|
||||
: t('website.ePayment.saveCredentials')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div className={styles.comingSoonGrid}>
|
||||
{COMING_SOON_GATEWAYS.map((gateway) => (
|
||||
<section key={gateway.id} className={styles.comingSoonCard}>
|
||||
@@ -354,6 +509,6 @@ export function WebsiteEPaymentPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</WebsiteSectionShell>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -468,6 +468,7 @@ function StoreItemsPageContent() {
|
||||
<StoreItemDiscountModal
|
||||
open={!!discountTarget}
|
||||
productTitle={discountTarget?.productTitle ?? ''}
|
||||
productNameFa={discountTarget?.productNameFa ?? ''}
|
||||
items={productItems}
|
||||
onClose={() => setDiscountTarget(null)}
|
||||
onSaved={mergeUpdatedItems}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<button
|
||||
type="button"
|
||||
className={fabStyles.addFab}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
onClick={() => {
|
||||
setError('')
|
||||
setCreateOpen(true)
|
||||
}}
|
||||
aria-label={t('website.specialItems.addFab')}
|
||||
>
|
||||
<Plus size={24} />
|
||||
@@ -376,6 +375,7 @@ export function StoreSpecialsPage() {
|
||||
title={t('website.specialItems.createTitle')}
|
||||
submitLabel={t('website.create')}
|
||||
isSubmitting={isSaving}
|
||||
error={createOpen ? error : undefined}
|
||||
/>
|
||||
|
||||
<StoreSpecialModal
|
||||
@@ -387,6 +387,7 @@ export function StoreSpecialsPage() {
|
||||
title={t('website.specialItems.editTitle')}
|
||||
submitLabel={t('website.save')}
|
||||
isSubmitting={isSaving}
|
||||
error={editSpecialTarget ? error : undefined}
|
||||
/>
|
||||
|
||||
<PickStoreSpecialItemsModal
|
||||
@@ -410,6 +411,7 @@ export function StoreSpecialsPage() {
|
||||
<StoreItemDiscountModal
|
||||
open={!!discountTarget}
|
||||
productTitle={discountTarget?.productTitle ?? ''}
|
||||
productNameFa={discountTarget?.productNameFa ?? ''}
|
||||
items={productItems}
|
||||
onClose={() => setDiscountTarget(null)}
|
||||
onSaved={mergeUpdatedItems}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
BellRing,
|
||||
CircleHelp,
|
||||
Award,
|
||||
CreditCard,
|
||||
Images,
|
||||
LayoutGrid,
|
||||
Building2,
|
||||
@@ -79,13 +78,6 @@ const websiteSections: {
|
||||
linkKey: 'website.card.badges.link',
|
||||
href: '/website/badges',
|
||||
},
|
||||
{
|
||||
icon: CreditCard,
|
||||
titleKey: 'nav.website.ePayment',
|
||||
descKey: 'website.card.ePayment.desc',
|
||||
linkKey: 'website.card.ePayment.link',
|
||||
href: '/website/e-payment',
|
||||
},
|
||||
]
|
||||
|
||||
export function WebsitePage() {
|
||||
|
||||
@@ -36,6 +36,12 @@ export interface MellatGatewaySettings {
|
||||
passwordSet?: boolean
|
||||
}
|
||||
|
||||
export interface ZarinpalGatewaySettings {
|
||||
enabled: boolean
|
||||
merchantId: string
|
||||
sandbox: boolean
|
||||
}
|
||||
|
||||
export interface StubGatewaySettings {
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -48,7 +54,7 @@ export interface EPaymentSettings {
|
||||
sep: StubGatewaySettings
|
||||
snappay: StubGatewaySettings
|
||||
digipay: StubGatewaySettings
|
||||
zarinpal: StubGatewaySettings
|
||||
zarinpal: ZarinpalGatewaySettings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +78,11 @@ export const DEFAULT_EPAYMENT_SETTINGS: EPaymentSettings = {
|
||||
sep: { enabled: false },
|
||||
snappay: { enabled: false },
|
||||
digipay: { enabled: false },
|
||||
zarinpal: { enabled: false },
|
||||
zarinpal: {
|
||||
enabled: false,
|
||||
merchantId: '',
|
||||
sandbox: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -77,3 +77,8 @@ html[data-theme='dark'] body::before {
|
||||
radial-gradient(ellipse 520px 520px at 72% 18%, rgba(255, 255, 255, 0.04), transparent 72%),
|
||||
radial-gradient(ellipse 420px 420px at 22% 82%, rgba(255, 255, 255, 0.03), transparent 72%);
|
||||
}
|
||||
|
||||
body:has([aria-modal='true']) #root {
|
||||
filter: blur(var(--modal-overlay-blur, 16px));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import { ApiError, isAbortError } from '../../lib/api'
|
||||
import {
|
||||
checkoutCart,
|
||||
getCart,
|
||||
getPaymentMethods,
|
||||
submitPaymentRedirect,
|
||||
type CheckoutPaymentType,
|
||||
type OnlineGatewayType,
|
||||
type PublicPaymentGateway,
|
||||
} from '../../services/cartService'
|
||||
import { syncGuestCartToServer } from '../../services/syncGuestCart'
|
||||
import { formatIrtPrice } from '../../utils/irtPrice'
|
||||
@@ -17,11 +19,6 @@ import { CheckoutActionBar } from '../../components/checkout/CheckoutActionBar'
|
||||
import { CheckoutBackButton } from '../../components/checkout/CheckoutBackButton'
|
||||
import styles from './CheckoutSteps.module.css'
|
||||
|
||||
const ONLINE_GATEWAYS: { id: OnlineGatewayType; label: string; ready: boolean }[] = [
|
||||
{ id: 'mellat', label: 'درگاه پرداخت بانک ملت - بهپرداخت', ready: true },
|
||||
{ id: 'sep', label: 'درگاه پرداخت بانک سامان - سامان کیش', ready: false },
|
||||
]
|
||||
|
||||
export function CheckoutPaymentStep() {
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
@@ -31,6 +28,8 @@ export function CheckoutPaymentStep() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [onlineGateways, setOnlineGateways] = useState<PublicPaymentGateway[]>([])
|
||||
const [onlinePaymentsEnabled, setOnlinePaymentsEnabled] = useState(false)
|
||||
const [paymentType, setPaymentType] = useState<CheckoutPaymentType>('e_payment_gate')
|
||||
const [gatewayType, setGatewayType] = useState<OnlineGatewayType>('mellat')
|
||||
const [transferAccount, setTransferAccount] = useState('')
|
||||
@@ -43,9 +42,13 @@ export function CheckoutPaymentStep() {
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
let data = await getCart(controller.signal)
|
||||
const [cartResult, methodsResult] = await Promise.all([
|
||||
getCart(controller.signal),
|
||||
getPaymentMethods(controller.signal).catch(() => null),
|
||||
])
|
||||
if (controller.signal.aborted) return
|
||||
|
||||
let data = cartResult
|
||||
if (data.cart.items.length === 0) {
|
||||
try {
|
||||
const synced = await syncGuestCartToServer(controller.signal)
|
||||
@@ -59,6 +62,23 @@ export function CheckoutPaymentStep() {
|
||||
setSubtotal(data.cart.subtotal)
|
||||
if (data.cart.items.length === 0) {
|
||||
navigate('/checkout/cart', { replace: true })
|
||||
return
|
||||
}
|
||||
|
||||
const gateways = methodsResult?.gateways ?? []
|
||||
const enabled = Boolean(methodsResult?.enabled && gateways.length > 0)
|
||||
setOnlineGateways(gateways)
|
||||
setOnlinePaymentsEnabled(enabled)
|
||||
|
||||
if (enabled) {
|
||||
const preferred =
|
||||
(methodsResult?.defaultGateway &&
|
||||
gateways.find((g) => g.id === methodsResult.defaultGateway)?.id) ||
|
||||
gateways[0]?.id
|
||||
if (preferred) setGatewayType(preferred)
|
||||
setPaymentType('e_payment_gate')
|
||||
} else {
|
||||
setPaymentType('transfer')
|
||||
}
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) return
|
||||
@@ -102,9 +122,8 @@ export function CheckoutPaymentStep() {
|
||||
}
|
||||
|
||||
if (paymentType === 'e_payment_gate') {
|
||||
const gateway = ONLINE_GATEWAYS.find((item) => item.id === gatewayType)
|
||||
if (!gateway?.ready) {
|
||||
setError('این درگاه هنوز فعال نیست. لطفاً ملت را انتخاب کنید.')
|
||||
if (!onlinePaymentsEnabled || !onlineGateways.some((g) => g.id === gatewayType)) {
|
||||
setError('درگاه پرداخت آنلاین در دسترس نیست. لطفاً روش دیگری انتخاب کنید.')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -227,27 +246,29 @@ export function CheckoutPaymentStep() {
|
||||
<div className={styles.field}>
|
||||
<label>روش پرداخت</label>
|
||||
<div className={styles.paymentOptions}>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
styles.paymentOption,
|
||||
paymentType === 'e_payment_gate' ? styles.paymentOptionActive : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => setPaymentType('e_payment_gate')}
|
||||
disabled={submitting}
|
||||
>
|
||||
<CreditCard size={20} />
|
||||
<span className={styles.paymentOptionBody}>
|
||||
<span className={styles.paymentOptionTitle}>پرداخت آنلاین</span>
|
||||
<span className={styles.paymentOptionHint}>پرداخت امن از طریق درگاه</span>
|
||||
</span>
|
||||
</button>
|
||||
{onlinePaymentsEnabled ? (
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
styles.paymentOption,
|
||||
paymentType === 'e_payment_gate' ? styles.paymentOptionActive : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => setPaymentType('e_payment_gate')}
|
||||
disabled={submitting}
|
||||
>
|
||||
<CreditCard size={20} />
|
||||
<span className={styles.paymentOptionBody}>
|
||||
<span className={styles.paymentOptionTitle}>پرداخت آنلاین</span>
|
||||
<span className={styles.paymentOptionHint}>پرداخت امن از طریق درگاه</span>
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{paymentType === 'e_payment_gate' && (
|
||||
{onlinePaymentsEnabled && paymentType === 'e_payment_gate' && (
|
||||
<div className={styles.paymentSubOptions} role="radiogroup" aria-label="انتخاب درگاه">
|
||||
{ONLINE_GATEWAYS.map((gateway) => {
|
||||
{onlineGateways.map((gateway) => {
|
||||
const active = gatewayType === gateway.id
|
||||
return (
|
||||
<button
|
||||
@@ -262,15 +283,12 @@ export function CheckoutPaymentStep() {
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => setGatewayType(gateway.id)}
|
||||
disabled={submitting || !gateway.ready}
|
||||
disabled={submitting}
|
||||
>
|
||||
<span className={styles.paymentSubRadio}>
|
||||
{active ? <span className={styles.paymentSubRadioInner} /> : null}
|
||||
</span>
|
||||
<span>
|
||||
{gateway.label}
|
||||
{!gateway.ready ? ' (بهزودی)' : ''}
|
||||
</span>
|
||||
<span>{gateway.labelFa || gateway.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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<PublicPaymentMethods>(paymentsMethodsPath(), {
|
||||
auth: false,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
/** Submit a gateway redirect (POST form or GET navigation). */
|
||||
export function submitPaymentRedirect(redirect: PaymentRedirect) {
|
||||
if (redirect.method === 'GET') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
@@ -644,9 +656,9 @@ export function UsersPage() {
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={tableStyles.th}>Name</th>
|
||||
<th className={`${tableStyles.th} ${styles.colName}`}>Name</th>
|
||||
<th className={tableStyles.th}>Cell number</th>
|
||||
<th className={tableStyles.th}>Role</th>
|
||||
<th className={`${tableStyles.th} ${styles.colRole}`}>Role</th>
|
||||
<th className={tableStyles.th}>Business</th>
|
||||
<th className={tableStyles.th}>Date joined</th>
|
||||
<th className={`${tableStyles.th} ${tableStyles.thActions}`}>Actions</th>
|
||||
@@ -672,12 +684,12 @@ export function UsersPage() {
|
||||
{!loading &&
|
||||
data?.items?.map((user) => (
|
||||
<tr key={user.id} className={!user.isActive ? styles.inactiveRow : undefined}>
|
||||
<td className={tableStyles.td}>
|
||||
<td className={`${tableStyles.td} ${styles.colName}`}>
|
||||
<div>{displayName(user)}</div>
|
||||
{!user.isActive && <div className={styles.statusInactive}>Inactive</div>}
|
||||
</td>
|
||||
<td className={tableStyles.td}>{formatCellForDisplay(user.cellNumber)}</td>
|
||||
<td className={tableStyles.td}>
|
||||
<td className={`${tableStyles.td} ${styles.colRole}`}>
|
||||
{(() => {
|
||||
const label = businessFilter
|
||||
? formatBusinessUserRoleLabel(user, teamRoles)
|
||||
@@ -697,7 +709,13 @@ export function UsersPage() {
|
||||
<div className={tableStyles.rowActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={tableStyles.controlBtn}
|
||||
className={`${tableStyles.controlBtn} ${
|
||||
isSuperAdminUser(user)
|
||||
? styles.shieldSuperAdmin
|
||||
: isBusinessOwnerUser(user)
|
||||
? styles.shieldOwner
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => openRoleChange(user)}
|
||||
title={
|
||||
businessFilter && user.isBusinessOwner
|
||||
@@ -711,7 +729,14 @@ export function UsersPage() {
|
||||
}
|
||||
disabled={!user.isActive || Boolean(businessFilter && user.isBusinessOwner)}
|
||||
>
|
||||
<Shield size={16} />
|
||||
<Shield
|
||||
size={16}
|
||||
fill={
|
||||
isSuperAdminUser(user) || isBusinessOwnerUser(user)
|
||||
? 'currentColor'
|
||||
: 'none'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
.deployStarted {
|
||||
color: #b45309;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.deployFail {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **For AI agents:** Read this file at the start of a new chat before making changes.
|
||||
> Update this document when a major feature is completed or architecture changes.
|
||||
|
||||
Last updated: August 14, 2026
|
||||
Last updated: August 16, 2026
|
||||
|
||||
---
|
||||
|
||||
@@ -222,7 +222,7 @@ Add to `/etc/hosts` (one line per tenant):
|
||||
| `/website/subscriptions` | Subscriptions | Placeholder |
|
||||
| `/website/faq` | FAQ | Placeholder |
|
||||
| `/website/badges` | Badges | Placeholder |
|
||||
| `/website/e-payment` | E-payment | Mellat credentials + multi-gateway stubs (SEP, Snapp Pay, DigiPay, Zarinpal) |
|
||||
| `/settings` | General settings | E-payment: Mellat + ZarinPal credentials; stubs for SEP / Snapp Pay / DigiPay |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user