Add business modules column and selectable home charts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 10:40:58 +03:30
co-authored by Cursor
parent a48a24e6f2
commit 0a2358b0c2
15 changed files with 788 additions and 137 deletions
+205 -2
View File
@@ -55,9 +55,22 @@ import {
getBusinessSettings,
updateBusinessBranding,
updateBusinessDefaultLocale,
updateBusinessModules,
updateBusinessPrimaryColor,
type DashboardLocale,
} from '../services/businessSettingsService'
import {
BUSINESS_MODULE_IDS,
BUSINESS_MODULE_LABELS,
DEFAULT_ENABLED_BUSINESS_MODULES,
DEFAULT_HOME_CHARTS,
HOME_CHART_IDS,
HOME_CHART_LABELS,
normalizeEnabledBusinessModules,
normalizeHomeCharts,
type BusinessModuleId,
type HomeChartId,
} from '../utils/businessModules'
import { flattenBusinessCategories } from '../utils/categories'
import { Pagination } from '@meshkee/dashboard-ui'
import pageStyles from '../components/PageContent.module.css'
@@ -70,6 +83,15 @@ function normalizeDefaultLocale(value: unknown): DashboardLocale {
return value === 'en' || value === 'fa' ? value : DEFAULT_LOCALE
}
function emptyModuleSelection(
enabled: BusinessModuleId[] = DEFAULT_ENABLED_BUSINESS_MODULES,
): Record<BusinessModuleId, boolean> {
const selected = new Set(enabled)
return Object.fromEntries(
BUSINESS_MODULE_IDS.map((id) => [id, selected.has(id)]),
) as Record<BusinessModuleId, boolean>
}
function formatDate(value: string) {
const d = new Date(value)
if (Number.isNaN(d.getTime())) return value
@@ -227,6 +249,17 @@ export function BusinessesPage() {
const [savingColorId, setSavingColorId] = useState<string | null>(null)
const [savingLocaleId, setSavingLocaleId] = useState<string | null>(null)
const [modulesOpen, setModulesOpen] = useState(false)
const [modulesBusiness, setModulesBusiness] = useState<BusinessListItem | null>(null)
const [modulesSelected, setModulesSelected] = useState<Record<BusinessModuleId, boolean>>(
() => emptyModuleSelection(),
)
const [modulesCharts, setModulesCharts] = useState<[HomeChartId, HomeChartId]>([
...DEFAULT_HOME_CHARTS,
])
const [modulesSubmitting, setModulesSubmitting] = useState(false)
const [modulesError, setModulesError] = useState('')
const [migrateOpen, setMigrateOpen] = useState(false)
const [migrateBusiness, setMigrateBusiness] = useState<BusinessListItem | null>(null)
const [migrateOldId, setMigrateOldId] = useState('')
@@ -465,6 +498,62 @@ export function BusinessesPage() {
}
}
function openModules(b: BusinessListItem) {
setModulesBusiness(b)
setModulesSelected(
emptyModuleSelection(
normalizeEnabledBusinessModules(b.enabledModules ?? DEFAULT_ENABLED_BUSINESS_MODULES),
),
)
setModulesCharts(normalizeHomeCharts(b.homeCharts ?? DEFAULT_HOME_CHARTS))
setModulesError('')
setModulesOpen(true)
}
function closeModules() {
if (modulesSubmitting) return
setModulesOpen(false)
setModulesBusiness(null)
setModulesError('')
}
async function submitModules() {
if (!modulesBusiness) return
const enabled = BUSINESS_MODULE_IDS.filter((id) => modulesSelected[id])
const charts = normalizeHomeCharts(modulesCharts)
setModulesSubmitting(true)
setModulesError('')
try {
await updateBusinessModules(modulesBusiness.id, enabled, charts)
setData((prev) =>
prev
? {
...prev,
items: prev.items.map((item) =>
item.id === modulesBusiness.id
? {
...item,
enabledModules: enabled,
moduleCount: enabled.length,
homeCharts: charts,
}
: item,
),
}
: prev,
)
showToast('Modules updated.', 'success')
setModulesOpen(false)
setModulesBusiness(null)
} catch (err) {
setModulesError(
err instanceof ApiError ? err.message : 'Unable to update modules.',
)
} finally {
setModulesSubmitting(false)
}
}
async function submitEdit() {
if (!editBusiness) return
setEditSubmitting(true)
@@ -903,13 +992,14 @@ export function BusinessesPage() {
<th className={styles.th}>Owner</th>
<th className={`${styles.th} ${styles.thTheme}`}>Theme</th>
<th className={`${styles.th} ${styles.thLocale}`}>Lang</th>
<th className={`${styles.th} ${styles.thModules}`}>Modules</th>
<th className={`${styles.th} ${styles.thActions}`}>Actions</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td className={styles.td} colSpan={7}>
<td className={styles.td} colSpan={8}>
Loading...
</td>
</tr>
@@ -917,7 +1007,7 @@ export function BusinessesPage() {
{!loading && data?.items?.length === 0 && (
<tr>
<td className={styles.td} colSpan={7}>
<td className={styles.td} colSpan={8}>
No results found.
</td>
</tr>
@@ -992,6 +1082,21 @@ export function BusinessesPage() {
<option value="en">EN</option>
</select>
</td>
<td className={`${styles.td} ${styles.tdModules}`}>
<button
type="button"
className={styles.modulesCountBtn}
onClick={() => openModules(b)}
title="Edit modules"
aria-label={`Edit modules for ${b.name}`}
>
{typeof b.moduleCount === 'number'
? b.moduleCount
: normalizeEnabledBusinessModules(
b.enabledModules ?? DEFAULT_ENABLED_BUSINESS_MODULES,
).length}
</button>
</td>
<td className={`${styles.td} ${styles.tdActions}`}>
<div className={styles.rowActions}>
<span className={styles.toggleInActions}>
@@ -1212,6 +1317,104 @@ export function BusinessesPage() {
</div>
</Modal>
<Modal
open={modulesOpen}
title={
modulesBusiness
? `Modules — ${modulesBusiness.name}`
: 'Modules'
}
onClose={closeModules}
>
{modulesError ? (
<p className={styles.alertError} role="alert">
{modulesError}
</p>
) : null}
<p className={styles.helperText}>
Choose which optional modules this business can use. Customers, website,
and other general sections stay available for every business.
</p>
<div className={styles.modulesChecks}>
{BUSINESS_MODULE_IDS.map((id) => (
<label key={id} className={styles.entityCheck}>
<input
type="checkbox"
checked={modulesSelected[id]}
disabled={modulesSubmitting}
onChange={(e) =>
setModulesSelected((prev) => ({
...prev,
[id]: e.target.checked,
}))
}
/>
<span>{BUSINESS_MODULE_LABELS[id]}</span>
</label>
))}
</div>
<div className={styles.modulesCharts}>
<p className={styles.entityGroupLabel}>Home charts</p>
<div className={styles.field}>
<label htmlFor="modules-chart-1">Chart 1</label>
<select
id="modules-chart-1"
value={modulesCharts[0]}
disabled={modulesSubmitting}
onChange={(e) => {
const value = e.target.value
if (!HOME_CHART_IDS.includes(value as HomeChartId)) return
setModulesCharts([value as HomeChartId, modulesCharts[1]])
}}
>
{HOME_CHART_IDS.map((id) => (
<option key={id} value={id}>
{HOME_CHART_LABELS[id]}
</option>
))}
</select>
</div>
<div className={styles.field}>
<label htmlFor="modules-chart-2">Chart 2</label>
<select
id="modules-chart-2"
value={modulesCharts[1]}
disabled={modulesSubmitting}
onChange={(e) => {
const value = e.target.value
if (!HOME_CHART_IDS.includes(value as HomeChartId)) return
setModulesCharts([modulesCharts[0], value as HomeChartId])
}}
>
{HOME_CHART_IDS.map((id) => (
<option key={id} value={id}>
{HOME_CHART_LABELS[id]}
</option>
))}
</select>
</div>
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={closeModules}
disabled={modulesSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitModules()}
disabled={modulesSubmitting}
>
{modulesSubmitting ? 'Saving…' : 'Save modules'}
</button>
</div>
</Modal>
<Modal
open={migrateOpen}
title={