Add SMS templates UI, prepaid balance settings, and list filter layout.
Business settings manage templates and balance; Super Admin verifies templates and tops up via SMS settings; filters use a separate panel with even field gaps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8f0b4cef85
commit
0eb19bbebd
@@ -188,6 +188,315 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cancelBtn {
|
||||
min-height: var(--field-height);
|
||||
padding: 0 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--field-font-size);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-ui);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cancelBtn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.smsCreditRow {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.smsCreditRowOk {
|
||||
background: rgba(var(--primary-rgb) / 0.1);
|
||||
border-color: rgba(var(--primary-rgb) / 0.28);
|
||||
}
|
||||
|
||||
.smsCreditRowOk .smsCreditLabel,
|
||||
.smsCreditRowOk .smsCreditValue {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.smsCreditRowLow {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.28);
|
||||
}
|
||||
|
||||
.smsCreditRowLow .smsCreditLabel,
|
||||
.smsCreditRowLow .smsCreditValue {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.smsCreditLabel {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.smsCreditValue {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
.smsTemplatesSummary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.smsTemplatesSummaryText {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.smsTemplatesCounts {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.smsTemplatesHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.smsTemplatesHeader .saveBtn,
|
||||
.smsTemplatesSummary .saveBtn,
|
||||
.smsModalToolbar .saveBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.smsTemplatesTitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.smsModalToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.smsEditorField {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.smsEditorField > span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.smsVarChips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
|
||||
.smsVarChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(var(--primary-rgb) / 0.28);
|
||||
background: rgba(var(--primary-rgb) / 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-ui);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.smsVarChip:hover:not(:disabled) {
|
||||
background: rgba(var(--primary-rgb) / 0.16);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.smsVarChip:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.smsTemplatesModal {
|
||||
width: min(960px, calc(100vw - 40px));
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.smsTemplateList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.smsTemplateRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
.smsTemplateMeta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.smsTemplateTop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.smsTemplateName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: min(420px, 100%);
|
||||
}
|
||||
|
||||
.smsTemplateBody {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.smsTemplateActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.smsBadgeFree,
|
||||
.smsBadgePending,
|
||||
.smsBadgeVerified {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.smsBadgeFree {
|
||||
background: rgba(var(--primary-rgb) / 0.12);
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(var(--primary-rgb) / 0.28);
|
||||
}
|
||||
|
||||
.smsBadgePending {
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
color: #b45309;
|
||||
border: 1px solid rgba(245, 158, 11, 0.35);
|
||||
}
|
||||
|
||||
.smsBadgeVerified {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #047857;
|
||||
border: 1px solid rgba(16, 185, 129, 0.32);
|
||||
}
|
||||
|
||||
.smsEditor {
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.smsEditorActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.smsTemplatesSummary {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.smsTemplatesSummary .saveBtn,
|
||||
.smsModalToolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.smsModalToolbar .saveBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.smsSenderForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.smsSenderForm .saveBtn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.comingSoonGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -1,231 +1,12 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
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'
|
||||
import { SmsSettingsSection } from '../components/SmsSettingsSection'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
import { ApiError } from '../lib/api'
|
||||
import {
|
||||
DEFAULT_EPAYMENT_SETTINGS,
|
||||
getSettings,
|
||||
updateStoreSettings,
|
||||
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 './SettingsPage.module.css'
|
||||
|
||||
const COMING_SOON_GATEWAYS: {
|
||||
id: Exclude<PaymentGatewayId, 'mellat' | 'zarinpal'>
|
||||
nameKey: BusinessMessageKey
|
||||
descKey: BusinessMessageKey
|
||||
}[] = [
|
||||
{
|
||||
id: 'sep',
|
||||
nameKey: 'website.ePayment.gateway.sep',
|
||||
descKey: 'website.ePayment.gateway.sepDesc',
|
||||
},
|
||||
{
|
||||
id: 'snappay',
|
||||
nameKey: 'website.ePayment.gateway.snappay',
|
||||
descKey: 'website.ePayment.gateway.snappayDesc',
|
||||
},
|
||||
{
|
||||
id: 'digipay',
|
||||
nameKey: 'website.ePayment.gateway.digipay',
|
||||
descKey: 'website.ePayment.gateway.digipayDesc',
|
||||
},
|
||||
]
|
||||
|
||||
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)
|
||||
const [passwordDraft, setPasswordDraft] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [savingKey, setSavingKey] = useState<string | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
void loadSettings(controller.signal)
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
async function loadSettings(signal?: AbortSignal) {
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const data = await getSettings(signal)
|
||||
setEPayment(data.settings.store.ePayment ?? DEFAULT_EPAYMENT_SETTINGS)
|
||||
setPasswordDraft('')
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError(t('productSettings.errorLoad'))
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function patchEPayment(
|
||||
saving: string,
|
||||
next: EPaymentSettings,
|
||||
password?: string,
|
||||
) {
|
||||
setSavingKey(saving)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const payload: EPaymentSettings = {
|
||||
enabled: next.enabled,
|
||||
defaultGateway: next.defaultGateway,
|
||||
gateways: {
|
||||
mellat: {
|
||||
enabled: next.gateways.mellat.enabled,
|
||||
terminalId: next.gateways.mellat.terminalId,
|
||||
username: next.gateways.mellat.username,
|
||||
password: password ?? '',
|
||||
},
|
||||
sep: { enabled: next.gateways.sep.enabled },
|
||||
snappay: { enabled: next.gateways.snappay.enabled },
|
||||
digipay: { enabled: next.gateways.digipay.enabled },
|
||||
zarinpal: {
|
||||
enabled: next.gateways.zarinpal.enabled,
|
||||
merchantId: next.gateways.zarinpal.merchantId.trim(),
|
||||
sandbox: next.gateways.zarinpal.sandbox,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const data = await updateStoreSettings({ ePayment: payload })
|
||||
setEPayment(data.settings.store.ePayment ?? DEFAULT_EPAYMENT_SETTINGS)
|
||||
if (password) setPasswordDraft('')
|
||||
showToast(t('productSettings.saved'), 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError(t('productSettings.errorSave'))
|
||||
}
|
||||
} finally {
|
||||
setSavingKey(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMasterToggle(checked: boolean) {
|
||||
await patchEPayment('master', {
|
||||
...ePayment,
|
||||
enabled: checked,
|
||||
defaultGateway: checked
|
||||
? ePayment.defaultGateway ??
|
||||
(ePayment.gateways.mellat.enabled
|
||||
? 'mellat'
|
||||
: ePayment.gateways.zarinpal.enabled
|
||||
? 'zarinpal'
|
||||
: 'mellat')
|
||||
: ePayment.defaultGateway,
|
||||
})
|
||||
}
|
||||
|
||||
async function handleMellatToggle(checked: boolean) {
|
||||
await patchEPayment('mellat-enabled', {
|
||||
...ePayment,
|
||||
// Turning a gateway on implies online payments are allowed at checkout.
|
||||
enabled: checked ? true : ePayment.enabled,
|
||||
gateways: {
|
||||
...ePayment.gateways,
|
||||
mellat: { ...ePayment.gateways.mellat, enabled: checked },
|
||||
},
|
||||
defaultGateway: nextDefaultGateway(ePayment, 'mellat', checked),
|
||||
})
|
||||
}
|
||||
|
||||
async function handleZarinpalToggle(checked: boolean) {
|
||||
await patchEPayment('zarinpal-enabled', {
|
||||
...ePayment,
|
||||
enabled: checked ? true : ePayment.enabled,
|
||||
gateways: {
|
||||
...ePayment.gateways,
|
||||
zarinpal: { ...ePayment.gateways.zarinpal, enabled: checked },
|
||||
},
|
||||
defaultGateway: nextDefaultGateway(ePayment, 'zarinpal', checked),
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSaveMellatCredentials(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
const terminalId = ePayment.gateways.mellat.terminalId.trim()
|
||||
const username = ePayment.gateways.mellat.username.trim()
|
||||
|
||||
if (ePayment.gateways.mellat.enabled && (!terminalId || !username)) {
|
||||
setError(t('website.ePayment.error.credentials'))
|
||||
return
|
||||
}
|
||||
|
||||
await patchEPayment(
|
||||
'mellat-credentials',
|
||||
{
|
||||
...ePayment,
|
||||
gateways: {
|
||||
...ePayment.gateways,
|
||||
mellat: {
|
||||
...ePayment.gateways.mellat,
|
||||
terminalId,
|
||||
username,
|
||||
},
|
||||
},
|
||||
},
|
||||
passwordDraft.trim() || undefined,
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<main className={pageStyles.content}>
|
||||
@@ -243,249 +24,9 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <div className={sharedStyles.alertError}>{error}</div> : null}
|
||||
|
||||
{isLoading ? (
|
||||
<p className={sharedStyles.status}>{t('productSettings.loading')}</p>
|
||||
) : (
|
||||
<div className={styles.stack}>
|
||||
<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>
|
||||
{!ePayment.enabled &&
|
||||
(ePayment.gateways.mellat.enabled || ePayment.gateways.zarinpal.enabled) ? (
|
||||
<p className={styles.masterOffHint} role="status">
|
||||
{t('website.ePayment.masterOffHint')}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.gatewayCard} data-card-hover>
|
||||
<div className={styles.gatewayHeader}>
|
||||
<div>
|
||||
<h3 className={styles.gatewayTitle}>
|
||||
{t('website.ePayment.gateway.mellat')}
|
||||
</h3>
|
||||
<p className={styles.gatewayDesc}>
|
||||
{t('website.ePayment.gateway.mellatDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="epayment-mellat"
|
||||
checked={ePayment.gateways.mellat.enabled}
|
||||
disabled={savingKey !== null}
|
||||
onChange={(checked) => void handleMellatToggle(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className={`${styles.credentials} ${
|
||||
!ePayment.gateways.mellat.enabled ? styles.credentialsDisabled : ''
|
||||
}`}
|
||||
onSubmit={handleSaveMellatCredentials}
|
||||
aria-disabled={!ePayment.gateways.mellat.enabled}
|
||||
>
|
||||
<div className={`${fieldStyles.field} ${styles.field}`}>
|
||||
<label htmlFor="mellat-terminal">
|
||||
{t('website.ePayment.terminalId')}
|
||||
</label>
|
||||
<input
|
||||
id="mellat-terminal"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
dir="ltr"
|
||||
value={ePayment.gateways.mellat.terminalId}
|
||||
disabled={savingKey !== null || !ePayment.gateways.mellat.enabled}
|
||||
onChange={(e) =>
|
||||
setEPayment((current) => ({
|
||||
...current,
|
||||
gateways: {
|
||||
...current.gateways,
|
||||
mellat: {
|
||||
...current.gateways.mellat,
|
||||
terminalId: e.target.value,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${fieldStyles.field} ${styles.field}`}>
|
||||
<label htmlFor="mellat-username">
|
||||
{t('website.ePayment.username')}
|
||||
</label>
|
||||
<input
|
||||
id="mellat-username"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
dir="ltr"
|
||||
value={ePayment.gateways.mellat.username}
|
||||
disabled={savingKey !== null || !ePayment.gateways.mellat.enabled}
|
||||
onChange={(e) =>
|
||||
setEPayment((current) => ({
|
||||
...current,
|
||||
gateways: {
|
||||
...current.gateways,
|
||||
mellat: {
|
||||
...current.gateways.mellat,
|
||||
username: e.target.value,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${fieldStyles.field} ${styles.field}`}>
|
||||
<label htmlFor="mellat-password">
|
||||
{t('website.ePayment.password')}
|
||||
</label>
|
||||
<div className={styles.passwordWrap}>
|
||||
<input
|
||||
id="mellat-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
dir="ltr"
|
||||
placeholder={t('website.ePayment.passwordPlaceholderSet')}
|
||||
value={passwordDraft}
|
||||
disabled={savingKey !== null || !ePayment.gateways.mellat.enabled}
|
||||
onChange={(e) => setPasswordDraft(e.target.value)}
|
||||
/>
|
||||
<Tooltip
|
||||
label={
|
||||
showPassword
|
||||
? t('website.ePayment.hidePassword')
|
||||
: t('website.ePayment.showPassword')
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.passwordToggle}
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
disabled={savingKey !== null || !ePayment.gateways.mellat.enabled}
|
||||
aria-label={
|
||||
showPassword
|
||||
? t('website.ePayment.hidePassword')
|
||||
: t('website.ePayment.showPassword')
|
||||
}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionsInline}>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.saveBtn}
|
||||
disabled={savingKey !== null || !ePayment.gateways.mellat.enabled}
|
||||
>
|
||||
{savingKey === 'mellat-credentials'
|
||||
? t('storeSettings.saving')
|
||||
: t('website.ePayment.saveCredentials')}
|
||||
</button>
|
||||
</div>
|
||||
</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}
|
||||
onChange={(checked) => void handleZarinpalToggle(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className={`${styles.credentialsInline} ${
|
||||
!ePayment.gateways.zarinpal.enabled ? styles.credentialsDisabled : ''
|
||||
}`}
|
||||
onSubmit={handleSaveZarinpalCredentials}
|
||||
aria-disabled={!ePayment.gateways.zarinpal.enabled}
|
||||
>
|
||||
<label className={styles.inlineLabel} htmlFor="zarinpal-merchant">
|
||||
{t('website.ePayment.merchantId')}
|
||||
</label>
|
||||
<input
|
||||
id="zarinpal-merchant"
|
||||
className={styles.inlineInput}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
dir="ltr"
|
||||
spellCheck={false}
|
||||
placeholder={t('website.ePayment.merchantIdPlaceholder')}
|
||||
value={ePayment.gateways.zarinpal.merchantId}
|
||||
disabled={savingKey !== null || !ePayment.gateways.zarinpal.enabled}
|
||||
onChange={(e) =>
|
||||
setEPayment((current) => ({
|
||||
...current,
|
||||
gateways: {
|
||||
...current.gateways,
|
||||
zarinpal: {
|
||||
...current.gateways.zarinpal,
|
||||
merchantId: e.target.value,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.saveBtn}
|
||||
disabled={savingKey !== null || !ePayment.gateways.zarinpal.enabled}
|
||||
>
|
||||
{savingKey === 'zarinpal-credentials'
|
||||
? t('storeSettings.saving')
|
||||
: t('website.ePayment.saveCredentials')}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div className={styles.comingSoonGrid}>
|
||||
{COMING_SOON_GATEWAYS.map((gateway) => (
|
||||
<section key={gateway.id} className={styles.comingSoonCard}>
|
||||
<div className={styles.gatewayHeader}>
|
||||
<div>
|
||||
<h3 className={styles.gatewayTitle}>{t(gateway.nameKey)}</h3>
|
||||
<p className={styles.gatewayDesc}>{t(gateway.descKey)}</p>
|
||||
</div>
|
||||
<span className={styles.badge}>{t('website.ePayment.comingSoon')}</span>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.stack}>
|
||||
<SmsSettingsSection />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ePaymentBlock {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.field,
|
||||
.sitemapActions {
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
} from '../services/settingsService'
|
||||
import { syncWebsiteSitemapConfig } from '../services/websiteSitemapService'
|
||||
import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules'
|
||||
import { EPaymentSettingsSection } from '../components/EPaymentSettingsSection'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import sharedStyles from './ProductSettingsPage.module.css'
|
||||
import settingsStyles from './SettingsPage.module.css'
|
||||
import styles from './WebsiteSettingsPage.module.css'
|
||||
|
||||
const SOURCE_OPTIONS: SpecialProductsSource[] = ['store_item', 'product']
|
||||
@@ -216,6 +218,10 @@ export function WebsiteSettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className={`${styles.ePaymentBlock} ${settingsStyles.stack}`}>
|
||||
<EPaymentSettingsSection />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user