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:
co-authored by
Cursor
parent
65012717d0
commit
e9400c2281
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user