Polish business FA layout and wire special-group keys plus SSL sync.

RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-03 00:06:44 +03:30
co-authored by Cursor
parent 66004a0fba
commit 672091d1f5
113 changed files with 4154 additions and 1451 deletions
+118 -80
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { CreditCard, Eye, RotateCcw, Search, Trash2 } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { OrderItemsModal } from '../components/OrderItemsModal'
@@ -8,6 +9,7 @@ import { OrderTransactionsModal } from '../components/OrderTransactionsModal'
import { Pagination } from '../components/Pagination'
import { Tooltip } from '../components/Tooltip'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
import { ApiError, isAbortError } from '../lib/api'
import { formatCellForDisplay } from '../lib/cellNumber'
import {
@@ -45,12 +47,13 @@ function displayName(order: Order) {
return name || '—'
}
function formatDateTime(value: string) {
function formatDateTime(value: string, locale: 'en' | 'fa') {
const d = new Date(value)
if (Number.isNaN(d.getTime())) return { date: value, time: '' }
const intlLocale = locale === 'fa' ? 'fa-IR' : 'en-US'
return {
date: d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' }),
time: d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
date: d.toLocaleDateString(intlLocale, { year: 'numeric', month: 'short', day: '2-digit' }),
time: d.toLocaleTimeString(intlLocale, { hour: '2-digit', minute: '2-digit' }),
}
}
@@ -58,16 +61,39 @@ function totalItemQuantity(order: Order) {
return order.items.reduce((sum, item) => sum + item.quantity, 0)
}
function sourceLabel(source: OrderSource) {
switch (source) {
case 'admin':
return 'Operator'
case 'app':
return 'Application'
case 'website':
default:
return 'Website'
function defaultFaForLabel(label: string | null | undefined) {
const trimmed = label?.trim()
if (!trimmed) return undefined
return DEFAULT_ORDER_PROCESS_STEPS.find(
(step) => step.label.toLowerCase() === trimmed.toLowerCase(),
)?.labelFa
}
function stepLabel(
steps: OrderProcessStep[],
processStepId: string,
processStepLabel?: string | null,
processStepLabelFa?: string | null,
locale: 'en' | 'fa' = 'en',
) {
const step =
steps.find((item) => item.id === processStepId) ??
DEFAULT_ORDER_PROCESS_STEPS.find((item) => item.id === processStepId)
if (locale === 'fa') {
let fa = processStepLabelFa?.trim() || step?.labelFa?.trim()
if (fa && processStepLabel?.trim() && fa === processStepLabel.trim()) {
fa = undefined
}
fa = fa || defaultFaForLabel(processStepLabel) || defaultFaForLabel(step?.label)
if (fa) return fa
}
if (processStepLabel?.trim()) return processStepLabel.trim()
return step?.label ?? processStepId
}
function sourceClass(source: OrderSource) {
@@ -82,15 +108,6 @@ function sourceClass(source: OrderSource) {
}
}
function stepLabel(
steps: OrderProcessStep[],
processStepId: string,
processStepLabel?: string | null,
) {
if (processStepLabel?.trim()) return processStepLabel.trim()
return steps.find((step) => step.id === processStepId)?.label ?? processStepId
}
function stepColor(
steps: OrderProcessStep[],
processStepId: string,
@@ -112,6 +129,8 @@ function stepColor(
}
export function OrdersPage() {
const t = useT()
const { locale } = useLocale()
const { showToast } = useToast()
const [data, setData] = useState<OrdersListResponse | null>(null)
const [loading, setLoading] = useState(false)
@@ -134,6 +153,18 @@ export function OrdersPage() {
const [removeTarget, setRemoveTarget] = useState<Order | null>(null)
const [removing, setRemoving] = useState(false)
function sourceLabel(source: OrderSource) {
switch (source) {
case 'admin':
return t('orders.source.operator')
case 'app':
return t('orders.source.app')
case 'website':
default:
return t('orders.source.website')
}
}
useEffect(() => {
const controller = new AbortController()
@@ -169,7 +200,7 @@ export function OrdersPage() {
setData(result)
} catch (err) {
if (isAbortError(err) || controller.signal.aborted) return
setError(err instanceof ApiError ? err.message : 'Unable to load orders.')
setError(err instanceof ApiError ? err.message : t('orders.error.load'))
} finally {
if (!controller.signal.aborted) setLoading(false)
}
@@ -188,6 +219,7 @@ export function OrdersPage() {
appliedFilters.dateTo,
appliedFilters.minTotal,
appliedFilters.maxTotal,
t,
])
const totalPages = useMemo(() => {
@@ -210,7 +242,7 @@ export function OrdersPage() {
const maxTotal = parseIrtInput(draftMaxCost)
if (minTotal !== null && maxTotal !== null && minTotal > maxTotal) {
setError('Minimum cost cannot be greater than maximum cost.')
setError(t('orders.error.minMax'))
return
}
@@ -261,10 +293,10 @@ export function OrdersPage() {
items: prev.items.filter((item) => item.id !== removeTarget.id),
}
})
showToast(`Order ${removeTarget.orderNumber} has been removed.`, 'success')
showToast(t('orders.removed', { orderNumber: removeTarget.orderNumber }), 'success')
setRemoveTarget(null)
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to remove order.')
setError(err instanceof ApiError ? err.message : t('orders.error.remove'))
} finally {
setRemoving(false)
}
@@ -282,75 +314,75 @@ export function OrdersPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>Orders</h2>
<p className={pageStyles.pageSubtitle}>
Track customer orders placed through your website, application, or by operators.
</p>
<h2 className={pageStyles.pageTitle}>{t('orders.title')}</h2>
<p className={pageStyles.pageSubtitle}>{t('orders.subtitle')}</p>
</div>
</div>
<div className={filterStyles.filtersPanel}>
<div className={filterStyles.filtersTitle}>Filters</div>
<div className={filterStyles.filtersTitle}>{t('orders.filters')}</div>
<div className={filterStyles.filtersGrid}>
<div className={styles.ordersFiltersRow}>
<div className={`${filterStyles.field} ${styles.filterOrderId}`}>
<label htmlFor="filter-order-id">Order ID</label>
<input
id="filter-order-id"
value={draftOrderId}
onChange={(e) => setDraftOrderId(e.target.value)}
placeholder="ORD-..."
placeholder={t('orders.filter.orderId')}
aria-label={t('orders.filter.orderId')}
/>
</div>
<div className={`${filterStyles.field} ${styles.filterCustomer}`}>
<label htmlFor="filter-order-customer">Customer name or number</label>
<input
id="filter-order-customer"
value={draftCustomer}
onChange={(e) => setDraftCustomer(e.target.value)}
placeholder="Name or phone"
placeholder={t('orders.filter.customer')}
aria-label={t('orders.filter.customer')}
autoComplete="off"
/>
</div>
<div className={`${filterStyles.field} ${styles.filterDate}`}>
<label htmlFor="filter-order-date-from">Date from</label>
<input
id="filter-order-date-from"
type="date"
value={draftDateFrom}
onChange={(e) => setDraftDateFrom(e.target.value)}
aria-label={t('orders.filter.dateFrom')}
title={t('orders.filter.dateFrom')}
/>
</div>
<div className={`${filterStyles.field} ${styles.filterDate}`}>
<label htmlFor="filter-order-date-to">Date to</label>
<input
id="filter-order-date-to"
type="date"
value={draftDateTo}
onChange={(e) => setDraftDateTo(e.target.value)}
aria-label={t('orders.filter.dateTo')}
title={t('orders.filter.dateTo')}
/>
</div>
<div className={`${filterStyles.field} ${styles.filterCost}`}>
<label htmlFor="filter-order-min-cost">Min cost (IRT)</label>
<input
id="filter-order-min-cost"
type="text"
inputMode="numeric"
value={draftMinCost}
onChange={(e) => setDraftMinCost(formatIrtInput(e.target.value))}
placeholder="0"
placeholder={t('orders.filter.minCost')}
aria-label={t('orders.filter.minCost')}
autoComplete="off"
/>
</div>
<div className={`${filterStyles.field} ${styles.filterCost}`}>
<label htmlFor="filter-order-max-cost">Max cost (IRT)</label>
<input
id="filter-order-max-cost"
type="text"
inputMode="numeric"
value={draftMaxCost}
onChange={(e) => setDraftMaxCost(formatIrtInput(e.target.value))}
placeholder="0"
placeholder={t('orders.filter.maxCost')}
aria-label={t('orders.filter.maxCost')}
autoComplete="off"
/>
</div>
@@ -361,8 +393,8 @@ export function OrdersPage() {
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnPrimary}`}
onClick={applyFilters}
disabled={loading}
aria-label="Search"
title="Search"
aria-label={t('orders.search')}
title={t('orders.search')}
>
<Search size={18} />
</button>
@@ -371,8 +403,8 @@ export function OrdersPage() {
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnGhost}`}
onClick={clearFilters}
disabled={loading}
aria-label="Clear filters"
title="Clear filters"
aria-label={t('orders.clearFilters')}
title={t('orders.clearFilters')}
>
<RotateCcw size={18} />
</button>
@@ -383,15 +415,17 @@ export function OrdersPage() {
<div className={styles.tablePanel}>
<div className={styles.tableWrap}>
<div className={styles.tableHeader}>
<div className={styles.tableHeaderTitle}>Order list</div>
<div className={styles.tableHeaderTitle}>{t('orders.listTitle')}</div>
<div className={styles.meta}>
{data ? (
data.total > 0 ? (
<>
Showing {showingFrom} - {showingTo} of {data.total}
</>
t('orders.showing', {
from: showingFrom,
to: showingTo,
total: data.total,
})
) : (
'No orders'
t('orders.none')
)
) : (
' '
@@ -401,7 +435,7 @@ export function OrdersPage() {
{error && <div className={styles.errorBanner}>{error}</div>}
<table className={styles.table}>
<table className={styles.table} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
<colgroup>
<col className={styles.colOrderId} />
<col className={styles.colCustomer} />
@@ -414,21 +448,21 @@ export function OrdersPage() {
</colgroup>
<thead>
<tr>
<th className={styles.th}>Order ID</th>
<th className={styles.th}>Customer</th>
<th className={styles.th}>Items</th>
<th className={styles.th}>Total cost</th>
<th className={styles.th}>Date & time</th>
<th className={styles.th}>Step</th>
<th className={styles.th}>Registered by</th>
<th className={`${styles.th} ${styles.thActions}`}>Actions</th>
<th className={styles.th}>{t('orders.col.orderId')}</th>
<th className={styles.th}>{t('orders.col.customer')}</th>
<th className={styles.th}>{t('orders.col.items')}</th>
<th className={styles.th}>{t('orders.col.total')}</th>
<th className={styles.th}>{t('orders.col.date')}</th>
<th className={styles.th}>{t('orders.col.step')}</th>
<th className={styles.th}>{t('orders.col.source')}</th>
<th className={`${styles.th} ${styles.thActions}`}>{t('orders.col.actions')}</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td className={styles.td} colSpan={COLUMN_COUNT}>
Loading...
{t('orders.loading')}
</td>
</tr>
)}
@@ -436,15 +470,22 @@ export function OrdersPage() {
{!loading && data?.items?.length === 0 && (
<tr>
<td className={styles.td} colSpan={COLUMN_COUNT}>
No results found.
{t('orders.empty')}
</td>
</tr>
)}
{!loading &&
data?.items?.map((order) => {
const { date, time } = formatDateTime(order.createdAt)
const { date, time } = formatDateTime(order.createdAt, locale)
const itemQty = totalItemQuantity(order)
const currentStepLabel = stepLabel(
processSteps,
order.processStepId ?? processSteps[0]?.id ?? 'processing',
order.processStepLabel,
order.processStepLabelFa,
locale,
)
return (
<tr key={order.id}>
@@ -477,17 +518,9 @@ export function OrdersPage() {
),
)}
onClick={() => setStepOrder(order)}
aria-label={`Change step: ${stepLabel(
processSteps,
order.processStepId ?? processSteps[0]?.id ?? 'processing',
order.processStepLabel,
)}`}
aria-label={t('orders.changeStep', { step: currentStepLabel })}
>
{stepLabel(
processSteps,
order.processStepId ?? processSteps[0]?.id ?? 'processing',
order.processStepLabel,
)}
{currentStepLabel}
</button>
</td>
<td className={styles.td}>
@@ -497,32 +530,32 @@ export function OrdersPage() {
</td>
<td className={`${styles.td} ${styles.tdActions}`}>
<div className={styles.rowActions}>
<Tooltip label="View items">
<Tooltip label={t('orders.viewItems')}>
<button
type="button"
className={styles.actionBtn}
onClick={() => setViewOrder(order)}
aria-label="View items"
aria-label={t('orders.viewItems')}
>
<Eye size={15} />
</button>
</Tooltip>
<Tooltip label="Transaction details">
<Tooltip label={t('orders.transactions')}>
<button
type="button"
className={styles.actionBtn}
onClick={() => setTransactionsOrder(order)}
aria-label="Transaction details"
aria-label={t('orders.transactions')}
>
<CreditCard size={15} />
</button>
</Tooltip>
<Tooltip label="Remove order">
<Tooltip label={t('orders.remove')}>
<button
type="button"
className={`${styles.actionBtn} ${styles.actionBtnDanger}`}
onClick={() => setRemoveTarget(order)}
aria-label="Remove order"
aria-label={t('orders.remove')}
disabled={removing}
>
<Trash2 size={15} />
@@ -538,7 +571,12 @@ export function OrdersPage() {
<div className={styles.pagination}>
<div>
Page {page} / {totalPages} · {PAGE_SIZE} per page · {data?.total ?? 0} total
{t('orders.pageMeta', {
page,
totalPages,
pageSize: PAGE_SIZE,
total: data?.total ?? 0,
})}
</div>
<Pagination
currentPage={page}
@@ -573,10 +611,10 @@ export function OrdersPage() {
<ConfirmDeleteModal
open={removeTarget !== null}
title="Remove order?"
title={t('orders.deleteTitle')}
message={
removeTarget
? `Remove order ${removeTarget.orderNumber}? This cannot be undone.`
? t('orders.deleteMessage', { orderNumber: removeTarget.orderNumber })
: ''
}
onCancel={() => setRemoveTarget(null)}