Add business dashboard dark mode from Super Admin theme setting.

Login and in-app surfaces follow tenant themeMode, matching customer dark tokens, with themed switches and tighter card/table chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-31 13:40:40 +03:30
co-authored by Cursor
parent 65012717d0
commit e9400c2281
83 changed files with 586 additions and 193 deletions
+104 -33
View File
@@ -9,6 +9,7 @@ import {
Lock,
Pencil,
Plus,
RefreshCw,
RotateCcw,
Search,
Trash2,
@@ -112,28 +113,43 @@ function emptyModuleSelection(
) as Record<BusinessModuleId, boolean>
}
/** Next anniversary of the contract date (renews every year). */
function startOfLocalDay(d: Date) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
}
/** Anniversary of the contract date. Does not roll a missed date into next year. */
function nextAnnualRenewal(contractAt: string, now = new Date()) {
const contract = new Date(contractAt)
if (Number.isNaN(contract.getTime())) return null
const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
let next = new Date(
startToday.getFullYear(),
contract.getMonth(),
contract.getDate(),
)
if (next.getTime() < startToday.getTime()) {
next = new Date(
startToday.getFullYear() + 1,
contract.getMonth(),
contract.getDate(),
)
}
const startToday = startOfLocalDay(now)
const contractDay = startOfLocalDay(contract)
const due =
contractDay.getTime() > startToday.getTime()
? contractDay
: new Date(
startToday.getFullYear(),
contract.getMonth(),
contract.getDate(),
)
const daysRemaining = Math.round(
(next.getTime() - startToday.getTime()) / (24 * 60 * 60 * 1000),
(due.getTime() - startToday.getTime()) / (24 * 60 * 60 * 1000),
)
return { next, year: next.getFullYear(), daysRemaining }
return { next: due, year: due.getFullYear(), daysRemaining }
}
function oneYearFromTodayIso(from = new Date()) {
const next = new Date(
from.getFullYear() + 1,
from.getMonth(),
from.getDate(),
12,
0,
0,
)
return next.toISOString()
}
function toDateInputValue(iso: string | null | undefined): string {
@@ -165,12 +181,18 @@ function formatRenewalCell(
if (!next) {
return { dateLabel: '—', daysLabel: null, tone: null }
}
const overdue = next.daysRemaining < 0
const absDays = Math.abs(next.daysRemaining)
const daysLabel =
next.daysRemaining === 0
? 'Renews today'
: next.daysRemaining === 1
? '1 day left'
: `${next.daysRemaining} days left`
: overdue
? absDays === 1
? '1 day past'
: `${absDays} days past`
: next.daysRemaining === 1
? '1 day left'
: `${next.daysRemaining} days left`
return {
dateLabel: next.next.toLocaleDateString('en-US', {
month: 'short',
@@ -178,7 +200,7 @@ function formatRenewalCell(
year: 'numeric',
}),
daysLabel,
tone: renewalTone(next.daysRemaining),
tone: overdue ? ('danger' as const) : renewalTone(next.daysRemaining),
}
}
@@ -360,6 +382,7 @@ export function BusinessesPage() {
const [removeTarget, setRemoveTarget] = useState<BusinessListItem | null>(null)
const [togglingId, setTogglingId] = useState<string | null>(null)
const [savingColorId, setSavingColorId] = useState<string | null>(null)
const [savingRenewalId, setSavingRenewalId] = useState<string | null>(null)
const [savingLocaleId, setSavingLocaleId] = useState<string | null>(null)
const [modulesOpen, setModulesOpen] = useState(false)
@@ -861,14 +884,42 @@ export function BusinessesPage() {
setEditOpen(false)
setEditBusiness(null)
showToast('Business updated.', 'success')
await fetchList()
} catch (err) {
setEditError(err instanceof ApiError ? err.message : 'Unable to update business.')
setEditError(
err instanceof ApiError ? err.message : 'Unable to update business.',
)
} finally {
setEditSubmitting(false)
}
}
async function restartRenewalCountdown(b: BusinessListItem) {
const annualRenewalAt = oneYearFromTodayIso()
setSavingRenewalId(b.id)
try {
await updateBusiness(b.id, { annualRenewalAt })
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === b.id ? { ...item, annualRenewalAt } : item,
),
}
})
showToast(`Renewal countdown restarted for "${b.name}".`, 'success')
} catch (err) {
showToast(
err instanceof ApiError
? err.message
: 'Unable to restart renewal countdown.',
'error',
)
} finally {
setSavingRenewalId(null)
}
}
function openDomainEdit(b: BusinessListItem) {
setDomainBusiness(b)
setDomainId(b.domainId)
@@ -1457,17 +1508,37 @@ export function BusinessesPage() {
<div className={styles.renewalCell}>
<span className={styles.renewalDate}>{renewal.dateLabel}</span>
{renewal.daysLabel ? (
<span
className={`${styles.renewalDays} ${
renewal.tone === 'ok'
? styles.renewalOk
: renewal.tone === 'warn'
? styles.renewalWarn
: styles.renewalDanger
}`}
>
{renewal.daysLabel}
</span>
<div className={styles.renewalDaysRow}>
<span
className={`${styles.renewalDays} ${
renewal.tone === 'ok'
? styles.renewalOk
: renewal.tone === 'warn'
? styles.renewalWarn
: styles.renewalDanger
}`}
>
{renewal.daysLabel}
</span>
<button
type="button"
className={styles.renewalRefreshBtn}
onClick={() => void restartRenewalCountdown(b)}
disabled={savingRenewalId === b.id}
aria-label={`Restart renewal countdown for ${b.name}`}
title="Restart countdown"
>
<RefreshCw
size={12}
aria-hidden="true"
className={
savingRenewalId === b.id
? styles.renewalRefreshSpin
: undefined
}
/>
</button>
</div>
) : null}
</div>
</td>