Localize order modals, add dual logos, and improve admin order tools.
Farsi business order/variation UI, light/dark logo upload on business profile, payment order steps, variation editing, super-admin site links, and checkout/auth fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
dc570c1e7f
commit
5e799eddf9
@@ -13,14 +13,22 @@ interface AddVariationModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSubmit: (variation: Omit<Variation, 'id'>) => void
|
||||
/** When set, form opens in edit mode with values pre-filled. */
|
||||
initial?: Variation | null
|
||||
}
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
|
||||
export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModalProps) {
|
||||
function seedCustomValues(values: string[]) {
|
||||
const trimmed = values.map((value) => value.trim()).filter(Boolean)
|
||||
return trimmed.length ? trimmed : ['']
|
||||
}
|
||||
|
||||
export function AddVariationModal({ open, onClose, onSubmit, initial = null }: AddVariationModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const isEdit = Boolean(initial)
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [type, setType] = useState<VariationType>('color')
|
||||
@@ -41,11 +49,21 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
if (open) {
|
||||
setMounted(true)
|
||||
setClosing(false)
|
||||
setType('color')
|
||||
setCustomName('')
|
||||
setSelectedColors([])
|
||||
setSelectedSizes([])
|
||||
setCustomValues([''])
|
||||
if (initial) {
|
||||
setType(initial.type)
|
||||
setCustomName(initial.type === 'custom' ? initial.name : '')
|
||||
setSelectedColors(initial.type === 'color' ? [...initial.values] : [])
|
||||
setSelectedSizes(initial.type === 'size' ? [...initial.values] : [])
|
||||
setCustomValues(
|
||||
initial.type === 'custom' ? seedCustomValues(initial.values) : [''],
|
||||
)
|
||||
} else {
|
||||
setType('color')
|
||||
setCustomName('')
|
||||
setSelectedColors([])
|
||||
setSelectedSizes([])
|
||||
setCustomValues([''])
|
||||
}
|
||||
} else if (mounted) {
|
||||
setClosing(true)
|
||||
const timer = setTimeout(() => {
|
||||
@@ -54,7 +72,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
}, ANIMATION_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [open, mounted])
|
||||
}, [open, mounted, initial])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted || closing) return
|
||||
@@ -128,7 +146,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={`${styles.modal} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||
className={`${styles.modal} ${styles.modalFormWide} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@@ -138,7 +156,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h3 id="add-variation-title" className={styles.title}>
|
||||
{t('categories.variations.addTitle')}
|
||||
{isEdit ? t('categories.variations.editTitle') : t('categories.variations.addTitle')}
|
||||
</h3>
|
||||
<button className={styles.closeBtn} onClick={onClose} aria-label={t('common.close')}>
|
||||
<X size={20} />
|
||||
@@ -151,6 +169,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
<select
|
||||
id="variationType"
|
||||
value={type}
|
||||
disabled={isEdit}
|
||||
onChange={(e) => setType(e.target.value as VariationType)}
|
||||
>
|
||||
<option value="color">{t('categories.variations.type.color')}</option>
|
||||
@@ -250,7 +269,7 @@ export function AddVariationModal({ open, onClose, onSubmit }: AddVariationModal
|
||||
{t('categories.modal.cancel')}
|
||||
</button>
|
||||
<button type="submit" className={styles.submitBtn} disabled={!canSubmit}>
|
||||
{t('categories.variations.submit')}
|
||||
{isEdit ? t('categories.variations.saveEdit') : t('categories.variations.submit')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { X } from 'lucide-react'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { Order } from '../services/orderService'
|
||||
import { formatCellForDisplay } from '../lib/cellNumber'
|
||||
import { formatIrtPrice } from '../utils/irtPrice'
|
||||
@@ -30,6 +31,7 @@ function totalQuantity(order: Order) {
|
||||
}
|
||||
|
||||
export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps) {
|
||||
const t = useT()
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
|
||||
@@ -75,11 +77,16 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
<div className={modalStyles.header}>
|
||||
<div>
|
||||
<h2 id="order-items-title" className={modalStyles.title}>
|
||||
Order items
|
||||
{t('orderItems.title')}
|
||||
</h2>
|
||||
<p className={modalStyles.subtitle}>{order.orderNumber}</p>
|
||||
</div>
|
||||
<button type="button" className={modalStyles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<button
|
||||
type="button"
|
||||
className={modalStyles.closeBtn}
|
||||
onClick={onClose}
|
||||
aria-label={t('orderItems.close')}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,26 +94,27 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
<div className={`${modalStyles.body} ${styles.body}`}>
|
||||
<div className={styles.metaRow}>
|
||||
<span className={styles.metaItem}>
|
||||
Customer: <strong>{displayName(order)}</strong>
|
||||
{t('orderItems.customer')}: <strong>{displayName(order)}</strong>
|
||||
</span>
|
||||
<span className={styles.metaItem}>
|
||||
Phone: <strong>{formatCellForDisplay(order.customer.cellNumber)}</strong>
|
||||
{t('orderItems.phone')}:{' '}
|
||||
<strong>{formatCellForDisplay(order.customer.cellNumber)}</strong>
|
||||
</span>
|
||||
<span className={styles.metaItem}>
|
||||
Items: <strong>{itemCount}</strong>
|
||||
{t('orderItems.items')}: <strong>{itemCount}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{order.items.length === 0 ? (
|
||||
<p className={styles.empty}>No items in this order.</p>
|
||||
<p className={styles.empty}>{t('orderItems.empty')}</p>
|
||||
) : (
|
||||
<div className={styles.tableBlock}>
|
||||
<div className={styles.tableHead}>
|
||||
<span aria-hidden="true" />
|
||||
<span>Product</span>
|
||||
<span>Qty</span>
|
||||
<span>Unit price</span>
|
||||
<span>Line total</span>
|
||||
<span>{t('orderItems.product')}</span>
|
||||
<span>{t('orderItems.qty')}</span>
|
||||
<span>{t('orderItems.unitPrice')}</span>
|
||||
<span>{t('orderItems.lineTotal')}</span>
|
||||
</div>
|
||||
<ul className={styles.itemList}>
|
||||
{order.items.map((item) => (
|
||||
@@ -122,7 +130,9 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
<div className={styles.itemTitle}>{item.productTitle}</div>
|
||||
<div className={styles.itemVariant}>{formatVariantLabel(item.selections)}</div>
|
||||
{item.variantSku && (
|
||||
<div className={styles.itemSku}>SKU: {item.variantSku}</div>
|
||||
<div className={styles.itemSku}>
|
||||
{t('orderItems.sku', { sku: item.variantSku })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.qtyCell}>{item.quantity}</div>
|
||||
@@ -136,20 +146,23 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
|
||||
|
||||
<div className={styles.summary}>
|
||||
<span className={styles.summaryLabel}>
|
||||
Order total · {itemCount} {itemCount === 1 ? 'item' : 'items'}
|
||||
{t('orderItems.summary', {
|
||||
count: itemCount,
|
||||
itemsLabel:
|
||||
itemCount === 1 ? t('orderItems.item') : t('orderItems.itemsPlural'),
|
||||
})}
|
||||
</span>
|
||||
<span className={styles.summaryValue}>{formatIrtPrice(order.total)}</span>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.actions}>
|
||||
<button type="button" className={modalStyles.cancelBtn} onClick={onClose}>
|
||||
Close
|
||||
{t('orderItems.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
,
|
||||
document.body,
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
@@ -55,8 +55,8 @@
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
background: rgba(var(--primary-rgb) / 0.12);
|
||||
color: #111827;
|
||||
background: #e5e7eb;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { updateOrder, type Order } from '../services/orderService'
|
||||
import type { OrderProcessStep } from '../services/settingsService'
|
||||
@@ -19,6 +21,14 @@ interface OrderStepModalProps {
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
|
||||
function stepDisplayLabel(step: OrderProcessStep, locale: string) {
|
||||
if (locale === 'fa') {
|
||||
const fa = step.labelFa?.trim()
|
||||
if (fa) return fa
|
||||
}
|
||||
return step.label
|
||||
}
|
||||
|
||||
export function OrderStepModal({
|
||||
open,
|
||||
order,
|
||||
@@ -26,6 +36,8 @@ export function OrderStepModal({
|
||||
onClose,
|
||||
onSaved,
|
||||
}: OrderStepModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const { showToast } = useToast()
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
@@ -70,11 +82,11 @@ export function OrderStepModal({
|
||||
|
||||
try {
|
||||
const result = await updateOrder(order.id, { processStepId: stepId })
|
||||
showToast(`Order ${order.orderNumber} step updated.`, 'success')
|
||||
showToast(t('orderStep.updated', { orderNumber: order.orderNumber }), 'success')
|
||||
onSaved?.(result.order)
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to update order step.')
|
||||
setError(err instanceof ApiError ? err.message : t('orderStep.error'))
|
||||
} finally {
|
||||
setSavingStepId(null)
|
||||
}
|
||||
@@ -82,9 +94,15 @@ export function OrderStepModal({
|
||||
|
||||
if (!mounted || !order) return null
|
||||
|
||||
const currentStep =
|
||||
steps.find((step) => step.id === order.processStepId) ??
|
||||
null
|
||||
const currentLabel =
|
||||
(locale === 'fa'
|
||||
? order.processStepLabelFa?.trim() || currentStep?.labelFa?.trim()
|
||||
: null) ||
|
||||
order.processStepLabel?.trim() ||
|
||||
steps.find((step) => step.id === order.processStepId)?.label ||
|
||||
currentStep?.label ||
|
||||
order.processStepId
|
||||
|
||||
return createPortal(
|
||||
@@ -100,14 +118,19 @@ export function OrderStepModal({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="order-step-title"
|
||||
lang={locale === 'fa' ? 'fa' : undefined}
|
||||
dir={locale === 'fa' ? 'rtl' : undefined}
|
||||
>
|
||||
<div className={modalStyles.header}>
|
||||
<div>
|
||||
<h2 id="order-step-title" className={modalStyles.title}>
|
||||
Change order step
|
||||
{t('orderStep.title')}
|
||||
</h2>
|
||||
<p className={modalStyles.subtitle}>
|
||||
{order.orderNumber} · Current: {currentLabel}
|
||||
{t('orderStep.current', {
|
||||
orderNumber: order.orderNumber,
|
||||
step: currentLabel,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -115,7 +138,7 @@ export function OrderStepModal({
|
||||
className={modalStyles.closeBtn}
|
||||
onClick={onClose}
|
||||
disabled={Boolean(savingStepId)}
|
||||
aria-label="Close"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -128,11 +151,12 @@ export function OrderStepModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.stepList} role="listbox" aria-label="Order process step">
|
||||
<div className={styles.stepList} role="listbox" aria-label={t('orderStep.listAria')}>
|
||||
{steps.map((step, index) => {
|
||||
const selected = order.processStepId === step.id
|
||||
const saving = savingStepId === step.id
|
||||
const color = normalizeStepColor(step.color, defaultStepColorForId(step.id, index))
|
||||
const label = stepDisplayLabel(step, locale)
|
||||
return (
|
||||
<button
|
||||
key={step.id}
|
||||
@@ -147,7 +171,7 @@ export function OrderStepModal({
|
||||
<span className={styles.stepColorDot} style={{ backgroundColor: color }} />
|
||||
<span className={styles.stepNumber}>{index + 1}</span>
|
||||
<span className={styles.stepLabel}>
|
||||
{saving ? 'Saving...' : step.label}
|
||||
{saving ? t('orderStep.saving') : label}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
@@ -155,14 +179,11 @@ export function OrderStepModal({
|
||||
</div>
|
||||
|
||||
{!steps.length && (
|
||||
<p className={styles.emptyText}>
|
||||
No order steps configured. Add steps in Store settings first.
|
||||
</p>
|
||||
<p className={styles.emptyText}>{t('orderStep.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
,
|
||||
document.body,
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useMemo, 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 type { BusinessMessageKey } from '../i18n/messages'
|
||||
import type { Order, OrderTransaction, TransactionStatus, TransactionType } from '../services/orderService'
|
||||
import { formatIrtPrice } from '../utils/irtPrice'
|
||||
import modalStyles from './VariationsModal.module.css'
|
||||
@@ -15,19 +17,18 @@ interface OrderTransactionsModalProps {
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
|
||||
function transactionTypeLabel(type: TransactionType) {
|
||||
switch (type) {
|
||||
case 'pos':
|
||||
return 'POS'
|
||||
case 'cash':
|
||||
return 'Cash'
|
||||
case 'transfer':
|
||||
return 'Transfer'
|
||||
case 'e_payment_gate':
|
||||
return 'E-payment gateway'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
const TYPE_KEYS: Record<TransactionType, BusinessMessageKey> = {
|
||||
pos: 'orderTransactions.type.pos',
|
||||
cash: 'orderTransactions.type.cash',
|
||||
transfer: 'orderTransactions.type.transfer',
|
||||
e_payment_gate: 'orderTransactions.type.e_payment_gate',
|
||||
}
|
||||
|
||||
const STATUS_KEYS: Record<TransactionStatus, BusinessMessageKey> = {
|
||||
pending: 'orderTransactions.status.pending',
|
||||
completed: 'orderTransactions.status.completed',
|
||||
failed: 'orderTransactions.status.failed',
|
||||
refunded: 'orderTransactions.status.refunded',
|
||||
}
|
||||
|
||||
function statusClass(status: TransactionStatus) {
|
||||
@@ -56,34 +57,8 @@ function formatDateTime(value: string, locale: 'en' | 'fa') {
|
||||
})
|
||||
}
|
||||
|
||||
function transactionDetails(transaction: OrderTransaction, locale: 'en' | 'fa') {
|
||||
const rows: { label: string; value: string }[] = [
|
||||
{ label: 'Status', value: transaction.status },
|
||||
{ label: 'Date', value: formatDateTime(transaction.createdAt, locale) },
|
||||
]
|
||||
|
||||
if (transaction.type === 'pos' && transaction.posType) {
|
||||
rows.push({ label: 'POS type', value: transaction.posType })
|
||||
}
|
||||
if (transaction.type === 'transfer') {
|
||||
if (transaction.transferAccount) {
|
||||
rows.push({ label: 'Account number', value: transaction.transferAccount })
|
||||
}
|
||||
if (transaction.transferRefNumber) {
|
||||
rows.push({ label: 'Ref code', value: transaction.transferRefNumber })
|
||||
}
|
||||
}
|
||||
if (transaction.type === 'e_payment_gate' && transaction.gatewayType) {
|
||||
rows.push({ label: 'Gateway', value: transaction.gatewayType })
|
||||
}
|
||||
if (transaction.notes) {
|
||||
rows.push({ label: 'Notes', value: transaction.notes })
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export function OrderTransactionsModal({ open, order, onClose }: OrderTransactionsModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const dateLocale = locale === 'fa' ? 'fa' : 'en'
|
||||
const [mounted, setMounted] = useState(open)
|
||||
@@ -120,6 +95,40 @@ export function OrderTransactionsModal({ open, order, onClose }: OrderTransactio
|
||||
|
||||
if (!mounted || !order) return null
|
||||
|
||||
function transactionDetails(transaction: OrderTransaction) {
|
||||
const rows: { label: string; value: string; isStatus?: boolean }[] = [
|
||||
{
|
||||
label: t('orderTransactions.status'),
|
||||
value: t(STATUS_KEYS[transaction.status] ?? 'orderTransactions.status.pending'),
|
||||
isStatus: true,
|
||||
},
|
||||
{
|
||||
label: t('orderTransactions.date'),
|
||||
value: formatDateTime(transaction.createdAt, dateLocale),
|
||||
},
|
||||
]
|
||||
|
||||
if (transaction.type === 'pos' && transaction.posType) {
|
||||
rows.push({ label: t('orderTransactions.posType'), value: transaction.posType })
|
||||
}
|
||||
if (transaction.type === 'transfer') {
|
||||
if (transaction.transferAccount) {
|
||||
rows.push({ label: t('orderTransactions.account'), value: transaction.transferAccount })
|
||||
}
|
||||
if (transaction.transferRefNumber) {
|
||||
rows.push({ label: t('orderTransactions.refCode'), value: transaction.transferRefNumber })
|
||||
}
|
||||
}
|
||||
if (transaction.type === 'e_payment_gate' && transaction.gatewayType) {
|
||||
rows.push({ label: t('orderTransactions.gateway'), value: transaction.gatewayType })
|
||||
}
|
||||
if (transaction.notes) {
|
||||
rows.push({ label: t('orderTransactions.notes'), value: transaction.notes })
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`${modalStyles.overlay} ${closing ? modalStyles.overlayOut : modalStyles.overlayIn}`}
|
||||
@@ -135,33 +144,40 @@ export function OrderTransactionsModal({ open, order, onClose }: OrderTransactio
|
||||
<div className={modalStyles.header}>
|
||||
<div>
|
||||
<h2 id="order-transactions-title" className={modalStyles.title}>
|
||||
Transaction details
|
||||
{t('orderTransactions.title')}
|
||||
</h2>
|
||||
<p className={modalStyles.subtitle}>{order.orderNumber}</p>
|
||||
</div>
|
||||
<button type="button" className={modalStyles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<button
|
||||
type="button"
|
||||
className={modalStyles.closeBtn}
|
||||
onClick={onClose}
|
||||
aria-label={t('orderTransactions.close')}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`${modalStyles.body} ${styles.body}`}>
|
||||
{transactions.length === 0 ? (
|
||||
<p className={styles.empty}>No payment transactions recorded for this order.</p>
|
||||
<p className={styles.empty}>{t('orderTransactions.empty')}</p>
|
||||
) : (
|
||||
<ul className={styles.txList}>
|
||||
{transactions.map((transaction) => {
|
||||
const details = transactionDetails(transaction, dateLocale)
|
||||
const details = transactionDetails(transaction)
|
||||
return (
|
||||
<li key={transaction.id} className={styles.txCard}>
|
||||
<div className={styles.txHeader}>
|
||||
<div className={styles.txType}>{transactionTypeLabel(transaction.type)}</div>
|
||||
<div className={styles.txType}>
|
||||
{t(TYPE_KEYS[transaction.type] ?? 'orderTransactions.type.e_payment_gate')}
|
||||
</div>
|
||||
<div className={styles.txAmount}>{formatIrtPrice(transaction.amount)}</div>
|
||||
</div>
|
||||
<div className={styles.txMeta}>
|
||||
{details.map((detail) => (
|
||||
<div key={detail.label} className={styles.txMetaItem}>
|
||||
{detail.label}:{' '}
|
||||
{detail.label === 'Status' ? (
|
||||
{detail.isStatus ? (
|
||||
<span className={`${styles.statusBadge} ${statusClass(transaction.status)}`}>
|
||||
{detail.value}
|
||||
</span>
|
||||
@@ -179,20 +195,25 @@ export function OrderTransactionsModal({ open, order, onClose }: OrderTransactio
|
||||
|
||||
<div className={styles.summary}>
|
||||
<span className={styles.summaryLabel}>
|
||||
{transactions.length} {transactions.length === 1 ? 'transaction' : 'transactions'}
|
||||
{t('orderTransactions.summary', {
|
||||
count: transactions.length,
|
||||
txLabel:
|
||||
transactions.length === 1
|
||||
? t('orderTransactions.one')
|
||||
: t('orderTransactions.many'),
|
||||
})}
|
||||
</span>
|
||||
<span className={styles.summaryValue}>{formatIrtPrice(totalPaid)}</span>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.actions}>
|
||||
<button type="button" className={modalStyles.cancelBtn} onClick={onClose}>
|
||||
Close
|
||||
{t('orderTransactions.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
,
|
||||
document.body,
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
box-shadow: 0 20px 60px rgba(31, 38, 135, 0.15);
|
||||
}
|
||||
|
||||
.modalListWide {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.modalFormWide {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.modalIn { animation: modalFadeIn 0.25s ease forwards; }
|
||||
.modalOut { animation: modalFadeOut 0.22s ease forwards; }
|
||||
|
||||
@@ -136,9 +144,7 @@
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.45;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { Pencil, Plus, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
@@ -10,6 +10,7 @@ import { createId } from '../utils/id'
|
||||
import { textLocaleAttrs } from '../utils/textLocale'
|
||||
import { AddVariationModal } from './AddVariationModal'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import controlStyles from './CategoryRow.module.css'
|
||||
import styles from './VariationsModal.module.css'
|
||||
|
||||
interface VariationsModalProps {
|
||||
@@ -38,7 +39,8 @@ export function VariationsModal({
|
||||
const isFa = locale === 'fa'
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [formOpen, setFormOpen] = useState(false)
|
||||
const [editTarget, setEditTarget] = useState<Variation | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -49,7 +51,8 @@ export function VariationsModal({
|
||||
const timer = setTimeout(() => {
|
||||
setMounted(false)
|
||||
setClosing(false)
|
||||
setAddOpen(false)
|
||||
setFormOpen(false)
|
||||
setEditTarget(null)
|
||||
}, ANIMATION_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
@@ -58,11 +61,11 @@ export function VariationsModal({
|
||||
useEffect(() => {
|
||||
if (!mounted || closing) return
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && !addOpen) onClose()
|
||||
if (e.key === 'Escape' && !formOpen) onClose()
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [mounted, closing, addOpen, onClose])
|
||||
}, [mounted, closing, formOpen, onClose])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
@@ -74,13 +77,35 @@ export function VariationsModal({
|
||||
...list,
|
||||
{ ...variation, id: createId() },
|
||||
])
|
||||
setAddOpen(false)
|
||||
setFormOpen(false)
|
||||
setEditTarget(null)
|
||||
}
|
||||
|
||||
function handleEditSubmit(variation: Omit<Variation, 'id'>) {
|
||||
if (!editTarget) return
|
||||
onChange(
|
||||
list.map((item) =>
|
||||
item.id === editTarget.id ? { ...item, ...variation } : item,
|
||||
),
|
||||
)
|
||||
setFormOpen(false)
|
||||
setEditTarget(null)
|
||||
}
|
||||
|
||||
function handleRemove(id: string) {
|
||||
onChange(list.filter((variation) => variation.id !== id))
|
||||
}
|
||||
|
||||
function openAddForm() {
|
||||
setEditTarget(null)
|
||||
setFormOpen(true)
|
||||
}
|
||||
|
||||
function openEditForm(variation: Variation) {
|
||||
setEditTarget(variation)
|
||||
setFormOpen(true)
|
||||
}
|
||||
|
||||
function typeLabel(type: VariationType) {
|
||||
if (type === 'color') return t('categories.variations.type.color')
|
||||
if (type === 'size') return t('categories.variations.type.size')
|
||||
@@ -108,7 +133,7 @@ export function VariationsModal({
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={`${styles.modal} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||
className={`${styles.modal} ${styles.modalListWide} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@@ -164,6 +189,16 @@ export function VariationsModal({
|
||||
</div>
|
||||
<div className={styles.listItemEnd}>
|
||||
<span className={styles.listItemType}>{typeLabel(variation.type)}</span>
|
||||
<Tooltip label={t('categories.variations.edit')}>
|
||||
<button
|
||||
type="button"
|
||||
className={controlStyles.controlBtn}
|
||||
onClick={() => openEditForm(variation)}
|
||||
aria-label={t('categories.variations.edit')}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('categories.variations.remove')}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -183,7 +218,7 @@ export function VariationsModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="button" className={styles.addBtn} onClick={() => setAddOpen(true)}>
|
||||
<button type="button" className={styles.addBtn} onClick={openAddForm}>
|
||||
<Plus size={18} />
|
||||
{t('categories.variations.add')}
|
||||
</button>
|
||||
@@ -201,9 +236,13 @@ export function VariationsModal({
|
||||
</div>
|
||||
|
||||
<AddVariationModal
|
||||
open={addOpen}
|
||||
onClose={() => setAddOpen(false)}
|
||||
onSubmit={handleAdd}
|
||||
open={formOpen}
|
||||
initial={editTarget}
|
||||
onClose={() => {
|
||||
setFormOpen(false)
|
||||
setEditTarget(null)
|
||||
}}
|
||||
onSubmit={editTarget ? handleEditSubmit : handleAdd}
|
||||
/>
|
||||
</>,
|
||||
document.body,
|
||||
|
||||
Reference in New Issue
Block a user