fix(crm): admin surfaces follow the admin light/dark toggle, not the gallery theme (#620)

CRM + accounting admin surfaces read gallery-theme tokens — the
`text-theme`/`text-muted-theme` utility classes and raw `var(--color-surface|
text|surface-border|elevated)` inline styles — which `ThemeContext` writes as
inline `--color-*` on <html> for every route. Those inline vars beat the admin
`.dark` toggle, so e.g. the "Create passive customer" modal (#620) renders dark
while the admin is in light mode (and the reverse).

Repoint every CRM/accounting admin surface to Tailwind `dark:` classes so it
tracks the admin toggle deterministically:
- text-theme → text-neutral-900 dark:text-neutral-100; text-muted-theme →
  text-neutral-500 dark:text-neutral-400 (across the CRM list/detail/editor
  pages, hours, calendar, lineage, installments, CRM settings).
- modal/dropdown/chip/divider inline `var(--color-surface*)` → bg-white
  dark:bg-neutral-900 / border-neutral-200 dark:border-neutral-700 etc.
  (CustomerManagement + CustomerDetail modals = the #620 fix).
- toggle off-track surface-border → bg-neutral-300 dark:bg-neutral-600; brand
  accent ON-state kept (var(--color-accent)).
- CalendarPage FullCalendar chrome: scope local --cal-* vars under .fc / .dark
  .fc so the calendar follows the admin toggle (was reading gallery vars).
- PasswordResetModal bare neutrals + TaxReport Storno/Reissue badges gain dark
  pairings.

Brand accent/primary tokens and the branded admin login are intentionally left
on the gallery theme. The same leak exists in non-CRM admin areas (dashboard,
events) — out of scope here.
This commit is contained in:
Luca
2026-06-16 14:54:25 +02:00
parent 03fa3d8296
commit d3266a0d1c
23 changed files with 192 additions and 197 deletions
@@ -120,10 +120,10 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
return (
<div ref={containerRef} className="relative">
<label className="block text-sm font-medium text-theme mb-1">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">
{t('events.customerPicker.label', 'Customer accounts')}
</label>
<p className="text-xs text-muted-theme mb-2">{helpText}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{helpText}</p>
{/* Selected chips */}
{value.length > 0 && (
@@ -131,16 +131,11 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
{value.map((c) => (
<span
key={c.id}
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs"
style={{
backgroundColor: 'var(--color-elevated, #f5f5f5)',
color: 'var(--color-text)',
border: '1px solid var(--color-surface-border, #e5e5e5)',
}}
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 border border-neutral-200 dark:border-neutral-700"
>
<span className="font-medium">{c.displayName?.trim() || c.email}</span>
{c.displayName?.trim() && c.email !== c.displayName && (
<span className="text-muted-theme">· {c.email}</span>
<span className="text-neutral-500 dark:text-neutral-400">· {c.email}</span>
)}
{!disabled && (
<button
@@ -174,18 +169,14 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
{/* Dropdown */}
{isOpen && query.trim() !== '' && (
<div
className="absolute left-0 right-0 mt-1 z-20 rounded-lg shadow-lg border max-h-72 overflow-y-auto"
style={{
backgroundColor: 'var(--color-surface, #ffffff)',
borderColor: 'var(--color-surface-border, #e5e5e5)',
}}
className="absolute left-0 right-0 mt-1 z-20 rounded-lg shadow-lg border max-h-72 overflow-y-auto bg-white dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700"
>
{isSearching ? (
<div className="px-3 py-3 text-sm text-muted-theme">
<div className="px-3 py-3 text-sm text-neutral-500 dark:text-neutral-400">
{t('events.customerPicker.searching', 'Searching…')}
</div>
) : results.length === 0 ? (
<div className="px-3 py-3 text-sm text-muted-theme">
<div className="px-3 py-3 text-sm text-neutral-500 dark:text-neutral-400">
{t('events.customerPicker.noResults', 'No matches. Invite this customer from Clients → Accounts first.')}
</div>
) : (
@@ -197,7 +188,7 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
onClick={() => select(r)}
className="w-full text-left px-3 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700 flex items-center gap-2"
>
<UserPlus className="w-4 h-4 text-muted-theme flex-shrink-0" />
<UserPlus className="w-4 h-4 text-neutral-500 dark:text-neutral-400 flex-shrink-0" />
<span className="flex-1 truncate">{labelFor(r)}</span>
</button>
</li>
@@ -57,7 +57,7 @@ const QuotesPanel: React.FC<Props> = ({ customerAccountId }) => {
return (
<Card padding="lg">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<FileText className="w-5 h-5" /> {t('customers.detail.quotesSection', 'Quotes')}
</h2>
<div className="flex gap-2">
@@ -78,18 +78,18 @@ const QuotesPanel: React.FC<Props> = ({ customerAccountId }) => {
</div>
{isLoading ? <Loading /> : !data || data.quotes.length === 0 ? (
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.detail.noQuotes', 'No quotes for this customer yet.')}
</p>
) : (
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
{data.quotes.map((q) => (
<li key={q.id} className="py-2 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<Link to={`/admin/clients/quotes/${q.id}`} className="text-theme hover:underline font-mono text-sm">
<Link to={`/admin/clients/quotes/${q.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline font-mono text-sm">
{q.quoteNumber}
</Link>
<span className="text-xs text-muted-theme ml-2">{q.eventName || fmtDate(q.issueDate)}</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2">{q.eventName || fmtDate(q.issueDate)}</span>
</div>
<span className="text-sm tabular-nums">{formatMoney(Number(q.totalAmountMinor) / 100, q.currency)}</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
@@ -118,7 +118,7 @@ const ContractsPanel: React.FC<Props> = ({ customerAccountId }) => {
return (
<Card padding="lg">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<ScrollText className="w-5 h-5" /> {t('customers.detail.contractsSection', 'Contracts')}
</h2>
<div className="flex gap-2">
@@ -132,18 +132,18 @@ const ContractsPanel: React.FC<Props> = ({ customerAccountId }) => {
</div>
{isLoading ? <Loading /> : !data || data.contracts.length === 0 ? (
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.detail.noContracts', 'No contracts for this customer yet.')}
</p>
) : (
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
{data.contracts.map((c) => (
<li key={c.id} className="py-2 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<Link to={`/admin/clients/contracts/${c.id}`} className="text-theme hover:underline font-mono text-sm">
<Link to={`/admin/clients/contracts/${c.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline font-mono text-sm">
{c.contractNumber}
</Link>
<span className="text-xs text-muted-theme ml-2 truncate">{c.title || fmtDate(c.issueDate)}</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2 truncate">{c.title || fmtDate(c.issueDate)}</span>
</div>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
c.status === 'fully_signed' ? 'bg-green-100 text-green-800'
@@ -172,7 +172,7 @@ const InvoicesPanel: React.FC<Props> = ({ customerAccountId }) => {
return (
<Card padding="lg">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Receipt className="w-5 h-5" /> {t('customers.detail.billsSection', 'Invoices')}
</h2>
<div className="flex gap-2">
@@ -187,18 +187,18 @@ const InvoicesPanel: React.FC<Props> = ({ customerAccountId }) => {
</div>
{isLoading ? <Loading /> : !data || data.invoices.length === 0 ? (
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.detail.noBills', 'No invoices for this customer yet.')}
</p>
) : (
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
{data.invoices.map((inv) => (
<li key={inv.id} className="py-2 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<Link to={`/admin/clients/bills/${inv.id}`} className="text-theme hover:underline font-mono text-sm">
<Link to={`/admin/clients/bills/${inv.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline font-mono text-sm">
{inv.invoiceNumber}
</Link>
<span className="text-xs text-muted-theme ml-2">
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2">
{fmtDate(inv.dueDate)}
{inv.installmentTotal > 1 ? ` · ${inv.installmentIndex + 1}/${inv.installmentTotal}` : ''}
</span>
@@ -62,8 +62,8 @@ const Toggle: React.FC<ToggleProps> = ({ enabled, onChange, label, hint, icon: I
role="switch"
aria-checked={enabled}
onClick={onChange}
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
style={{ backgroundColor: enabled ? 'var(--color-accent, #5C8762)' : '#cbd5e1' }}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${enabled ? '' : 'bg-neutral-300 dark:bg-neutral-600'}`}
style={enabled ? { backgroundColor: 'var(--color-accent, #5C8762)' } : undefined}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`}
@@ -91,7 +91,7 @@ export const DocumentLineageCard: React.FC<DocumentLineageCardProps> = ({
if (isLoading) {
return (
<Card padding="md" className={className}>
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('dealLineage.loading', 'Loading related documents…')}
</p>
</Card>
@@ -128,7 +128,7 @@ export const DocumentLineageCard: React.FC<DocumentLineageCardProps> = ({
<h2 className="font-semibold mb-1 flex items-center gap-2">
{t('dealLineage.title', 'Related documents')}
</h2>
<p className="text-xs text-muted-theme">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{t('dealLineage.empty', 'No other documents share this deal yet. New invoices, contracts, or installments will show up here once created.')}
</p>
</Card>
@@ -256,7 +256,7 @@ const Group: React.FC<{
action?: React.ReactNode;
}> = ({ icon, label, count, children, action }) => (
<div className="mb-3 last:mb-0">
<div className="flex items-center gap-2 text-xs uppercase tracking-wider text-muted-theme mb-1">
<div className="flex items-center gap-2 text-xs uppercase tracking-wider text-neutral-500 dark:text-neutral-400 mb-1">
{icon}
<span>{label}</span>
<span>({count})</span>
@@ -282,19 +282,19 @@ const Row: React.FC<{
const inner = (
<div className="flex items-center justify-between gap-3 py-1.5">
<div className="flex items-center gap-2 min-w-0">
<span className={`font-mono text-sm ${isCurrent ? 'text-muted-theme' : ''}`}>{number}</span>
<span className={`font-mono text-sm ${isCurrent ? 'text-neutral-500 dark:text-neutral-400' : ''}`}>{number}</span>
{badge && (
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded font-semibold bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
{badge}
</span>
)}
{meta && (
<span className="text-xs text-muted-theme truncate">{meta}</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400 truncate">{meta}</span>
)}
</div>
<div className="flex items-center gap-2 shrink-0 text-xs">
{right && <span className="tabular-nums">{right}</span>}
<span className="text-muted-theme">{t(statusKey, statusFallback)}</span>
<span className="text-neutral-500 dark:text-neutral-400">{t(statusKey, statusFallback)}</span>
</div>
</div>
);
@@ -153,7 +153,7 @@ export const EditInstallmentPlanModal: React.FC<EditInstallmentPlanModalProps> =
<h2 className="text-xl font-semibold">
{t('dealLineage.editPlanModalTitle', 'Edit installment plan')}
</h2>
<p className="text-xs text-muted-theme mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('dealLineage.editPlanHelp',
'Atomically reshape this plan: change percents, labels, triggers, add or remove rows. The plan total stays fixed; existing invoice numbers are kept where possible. Refused once any invoice has shipped.')}
</p>
@@ -177,7 +177,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
{enabled && (
<>
<p className="text-xs text-muted-theme mb-3">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3">
{advanced
? t('installments.advancedHint',
'Pick a trigger (quote accepted, before/after event, on delivery, fixed date) plus offset in days. Triggers re-resolve if the event date later shifts.')
@@ -192,7 +192,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
className="grid grid-cols-12 gap-2 items-end p-2 rounded-md bg-neutral-50 dark:bg-neutral-800/40"
>
<div className="col-span-2">
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('installments.percent', '%')}
</label>
<Input
@@ -206,7 +206,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
/>
</div>
<div className="col-span-4">
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('installments.label', 'Label')}
</label>
<Input
@@ -219,11 +219,11 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
{!advanced ? (
<div className="col-span-5">
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('installments.sendOn', 'Send on')}
</label>
{row.trigger === 'after_delivery' ? (
<div className="text-xs text-muted-theme py-2">
<div className="text-xs text-neutral-500 dark:text-neutral-400 py-2">
{t('installments.onDeliveryHint',
'On delivery — admin releases manually. Switch to advanced to change.')}
</div>
@@ -242,7 +242,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
) : (
<>
<div className="col-span-3">
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('installments.trigger', 'Trigger')}
</label>
<select
@@ -262,7 +262,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
</select>
</div>
<div className="col-span-2">
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('installments.offsetDays', 'Offset (days)')}
</label>
<Input
@@ -87,12 +87,12 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<Card className="max-w-md w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-neutral-900">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{resultPassword ? t('events.passwordReset.newTitle') : t('events.passwordReset.title')}
</h2>
<button
onClick={onClose}
className="text-neutral-400 hover:text-neutral-600"
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
>
<X className="w-5 h-5" />
</button>
@@ -163,12 +163,12 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
type="checkbox"
checked={sendEmail}
onChange={(e) => setSendEmail(e.target.checked)}
className="w-4 h-4 text-accent bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500 focus:ring-2"
className="w-4 h-4 text-accent bg-neutral-100 dark:bg-neutral-700 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500 focus:ring-2"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-neutral-500" />
<span className="text-sm font-medium text-neutral-700">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('events.passwordReset.sendEmail')}
</span>
</div>
+59 -59
View File
@@ -301,13 +301,13 @@ export const CustomerDetailPage: React.FC = () => {
className="p-2 -ml-2 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
aria-label={t('common.back', 'Back')}
>
<ArrowLeft className="w-4 h-4 text-muted-theme" />
<ArrowLeft className="w-4 h-4 text-neutral-500 dark:text-neutral-400" />
</Link>
<div className="min-w-0">
<h1 className="text-2xl font-bold text-theme truncate">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100 truncate">
{customer.displayName || customer.email}
</h1>
<p className="text-sm text-muted-theme truncate">{customer.email}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400 truncate">{customer.email}</p>
</div>
</div>
<div className="flex flex-col items-end gap-1">
@@ -333,7 +333,7 @@ export const CustomerDetailPage: React.FC = () => {
{t('customers.passive.badge', 'Passive — admin only')}
</span>
) : (
<span className="text-[11px] text-muted-theme">
<span className="text-[11px] text-neutral-500 dark:text-neutral-400">
{t('customers.passive.activeLabel', 'Has portal access')}
</span>
)}
@@ -342,16 +342,16 @@ export const CustomerDetailPage: React.FC = () => {
{/* Account section */}
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-4 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
<Mail className="w-5 h-5" /> {t('customers.detail.accountSection', 'Account')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.email', 'Email')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.email', 'Email')}</label>
<Input type="email" value={form.email || ''} onChange={setField('email')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.preferredLanguage', 'Preferred language')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.preferredLanguage', 'Preferred language')}</label>
<select
value={form.preferredLanguage || profileDefaultLocale}
onChange={setField('preferredLanguage')}
@@ -375,12 +375,12 @@ export const CustomerDetailPage: React.FC = () => {
{/* Personal section */}
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-4">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
{t('customers.detail.personalSection', 'Personal information')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.salutation', 'Salutation')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.salutation', 'Salutation')}</label>
{/* Salutation values are stored verbatim in the DB ("Herr",
"Frau", "Mx", "Dr") — those are the canonical token values
across locales. Display labels are translated; the value
@@ -400,25 +400,25 @@ export const CustomerDetailPage: React.FC = () => {
</select>
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.firstName', 'First name')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.firstName', 'First name')}</label>
<Input value={form.firstName || ''} onChange={setField('firstName')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.lastName', 'Last name')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.lastName', 'Last name')}</label>
<Input value={form.lastName || ''} onChange={setField('lastName')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.displayName', 'Display name')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.displayName', 'Display name')}</label>
<Input value={form.displayName || ''} onChange={setField('displayName')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1 flex items-center gap-1">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1 flex items-center gap-1">
<Phone className="w-4 h-4" /> {t('customers.detail.phone', 'Phone')}
</label>
<Input value={form.phone || ''} onChange={setField('phone')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1 flex items-center gap-1">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1 flex items-center gap-1">
<Building2 className="w-4 h-4" /> {t('customers.detail.company', 'Company')}
</label>
<Input value={form.companyName || ''} onChange={setField('companyName')} />
@@ -437,10 +437,10 @@ export const CustomerDetailPage: React.FC = () => {
{/* Notes (admin-only) */}
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-4 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
<FileText className="w-5 h-5" /> {t('customers.detail.notesSection', 'Internal notes')}
</h2>
<p className="text-xs text-muted-theme mb-3">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3">
{t('customers.detail.notesHint', 'Visible only to admins. Never shown to the customer.')}
</p>
<textarea
@@ -454,7 +454,7 @@ export const CustomerDetailPage: React.FC = () => {
{/* Assigned events */}
<Card padding="lg">
<div className="flex items-center justify-between gap-4 mb-4 flex-wrap">
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Calendar className="w-5 h-5" /> {t('customers.detail.eventsSection', 'Assigned events')}
</h2>
{/* Manage galleries: opens the multi-select dialog that
@@ -473,17 +473,17 @@ export const CustomerDetailPage: React.FC = () => {
</Button>
</div>
{customer.events.length === 0 ? (
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.detail.noEvents', 'Not assigned to any events yet. Use "Manage galleries" to add some.')}
</p>
) : (
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
{customer.events.map((ev) => (
<li key={ev.id} className="py-2 flex items-center justify-between">
<Link to={`/admin/events/${ev.id}`} className="text-theme hover:underline">
<Link to={`/admin/events/${ev.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline">
{ev.eventName}
</Link>
<span className="text-xs text-muted-theme">
<span className="text-xs text-neutral-500 dark:text-neutral-400">
{ev.eventDate ? fmtDate(ev.eventDate) : ''}
{ev.expiresAt ? ` · ${t('customers.detail.expires', 'expires')} ${fmtDate(ev.expiresAt)}` : ''}
</span>
@@ -509,36 +509,36 @@ export const CustomerDetailPage: React.FC = () => {
{/* Address + billing */}
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-4 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
<MapPin className="w-5 h-5" /> {t('customers.detail.billingSection', 'Address & billing')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.billingEmail', 'Billing email')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.billingEmail', 'Billing email')}</label>
<Input type="email" value={form.billingEmail || ''} onChange={setField('billingEmail')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.vatId', 'VAT / tax ID')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.vatId', 'VAT / tax ID')}</label>
<Input value={form.vatId || ''} onChange={setField('vatId')} />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.addressLine1', 'Address line 1')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.addressLine1', 'Address line 1')}</label>
<Input value={form.addressLine1 || ''} onChange={setField('addressLine1')} />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.addressLine2', 'Address line 2')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.addressLine2', 'Address line 2')}</label>
<Input value={form.addressLine2 || ''} onChange={setField('addressLine2')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.postalCode', 'Postal code')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.postalCode', 'Postal code')}</label>
<Input value={form.postalCode || ''} onChange={setField('postalCode')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.city', 'City')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.city', 'City')}</label>
<Input value={form.city || ''} onChange={setField('city')} />
</div>
<div>
<label className="block text-sm font-medium text-theme mb-1">{t('customers.detail.state', 'State / region')}</label>
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">{t('customers.detail.state', 'State / region')}</label>
<Input value={form.state || ''} onChange={setField('state')} />
</div>
<div>
@@ -575,11 +575,11 @@ export const CustomerDetailPage: React.FC = () => {
the moment any master flag is re-enabled. */}
{(flags.calendar || flags.quotes || flags.bills || flags.hoursLogging || flags.contracts) && (
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-1 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-1 flex items-center gap-2">
<ToggleLeft className="w-5 h-5" />
{t('customers.detail.featuresSection', 'Customer features')}
</h2>
<p className="text-xs text-muted-theme mb-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
{t(
'customers.detail.featuresHint',
'Per-customer overrides for the customer-surface tabs. The global toggles in Settings → Features are the master switch — when global is OFF nobody sees the tab, regardless of what you set here. Defaults are ON, so flip a switch OFF to hide a tab for this specific customer.'
@@ -619,7 +619,7 @@ export const CustomerDetailPage: React.FC = () => {
const enabled = !!form[key];
return (
<label key={key} className="flex items-center justify-between gap-3 cursor-pointer">
<span className="text-sm font-medium text-theme flex items-center gap-2">
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
{t(labelKey, fallback)}
{/* Status pill — 'soon' = amber, 'new' = green.
Colors match Settings → Features StatusBadge so
@@ -639,8 +639,8 @@ export const CustomerDetailPage: React.FC = () => {
role="switch"
aria-checked={enabled}
onClick={() => toggleFeature(key)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
style={{ backgroundColor: enabled ? 'var(--color-accent)' : 'var(--color-surface-border)' }}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${enabled ? '' : 'bg-neutral-300 dark:bg-neutral-600'}`}
style={enabled ? { backgroundColor: 'var(--color-accent)' } : undefined}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`}
@@ -659,7 +659,7 @@ export const CustomerDetailPage: React.FC = () => {
per-entry basis from the standalone Hours logging page. */}
{flags.hoursLogging && form.featureHoursLogging && (
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<label className="block text-sm font-medium text-theme mb-1">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">
{t('customers.field.hourlyRate', 'Default hourly rate')}
</label>
<DecimalInput
@@ -674,7 +674,7 @@ export const CustomerDetailPage: React.FC = () => {
placeholder="150.00"
className="w-40 input"
/>
<p className="text-xs text-muted-theme mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('customers.field.hourlyRateHint',
'Major units (e.g. 150.00 for {{currency}} 150). Leave blank to require a per-entry override on every block.',
{ currency: profileDefaultCurrency })}
@@ -693,17 +693,17 @@ export const CustomerDetailPage: React.FC = () => {
off — admin has nothing to bill, so cadence is moot. */}
{flags.bills && (
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-1 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-1 flex items-center gap-2">
<Calendar className="w-5 h-5" />
{t('customers.billing.section', 'Billing cadence')}
</h2>
<p className="text-xs text-muted-theme mb-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
{t('customers.billing.hint',
'Per-event (default): every invoice is sent on its own schedule. Monthly: all invoices issued in the period accumulate into one bill that fires on the configured day.')}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-theme mb-1">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">
{t('customers.billing.cadence', 'Billing cadence')}
</label>
<select
@@ -719,7 +719,7 @@ export const CustomerDetailPage: React.FC = () => {
</div>
{(form.billingCadence === 'monthly' || form.billingCadence === 'quarterly') && (
<div>
<label className="block text-sm font-medium text-theme mb-1">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">
{t('customers.billing.cycleDay', 'Cycle day')}
</label>
<input
@@ -730,7 +730,7 @@ export const CustomerDetailPage: React.FC = () => {
onChange={(e) => setForm((prev) => ({ ...prev, billingCycleDay: Number(e.target.value) } as any))}
className="input w-full"
/>
<p className="text-xs text-muted-theme mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('customers.billing.cycleDayHint',
'1..28 = day of month. Use negative -1..-15 for "N days before month end" (so -3 fires on the 28th of a 31-day month).')}
</p>
@@ -741,7 +741,7 @@ export const CustomerDetailPage: React.FC = () => {
{/* Per-customer Skonto opt-out (migration 112). For B2B
customers who negotiated "no early-payment discount" — set
once instead of ticking the per-invoice toggle every time. */}
<label className="mt-4 flex items-start gap-2 text-sm text-theme">
<label className="mt-4 flex items-start gap-2 text-sm text-neutral-900 dark:text-neutral-100">
<input
type="checkbox"
checked={!!form.skontoDisabled}
@@ -750,7 +750,7 @@ export const CustomerDetailPage: React.FC = () => {
/>
<span>
{t('customers.billing.skontoDisabled', 'No Skonto for this customer')}
<span className="block text-xs text-muted-theme">
<span className="block text-xs text-neutral-500 dark:text-neutral-400">
{t('customers.billing.skontoDisabledHint',
'Disables the early-payment discount on all of this customers invoices, regardless of template or global defaults.')}
</span>
@@ -765,14 +765,14 @@ export const CustomerDetailPage: React.FC = () => {
{(form.billingCadence === 'monthly' || form.billingCadence === 'manual') && monthlyDraft && monthlyDraft.lineItems.length > 0 && (
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-theme">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{form.billingCadence === 'manual'
? t('customers.billing.draftPreview.titleManual',
'Pending — ships on manual trigger')
: t('customers.billing.draftPreview.title',
'Pending in this month\'s bill')}
</h3>
<span className="text-xs text-muted-theme">
<span className="text-xs text-neutral-500 dark:text-neutral-400">
{monthlyDraft.periodStart && monthlyDraft.periodEnd
? t('customers.billing.draftPreview.periodRange',
'{{number}} · {{from}} {{to}}',
@@ -806,10 +806,10 @@ export const CustomerDetailPage: React.FC = () => {
<tbody>
{monthlyDraft.lineItems.map((li) => (
<tr key={li.id} className="border-t border-neutral-200 dark:border-neutral-700">
<td className="px-3 py-1.5 tabular-nums text-muted-theme">{li.position}</td>
<td className="px-3 py-1.5 tabular-nums text-neutral-500 dark:text-neutral-400">{li.position}</td>
<td className="px-3 py-1.5">
{li.parentPosition != null && (
<span className="text-muted-theme mr-1"></span>
<span className="text-neutral-500 dark:text-neutral-400 mr-1"></span>
)}
{li.description}
</td>
@@ -863,7 +863,7 @@ export const CustomerDetailPage: React.FC = () => {
>
{t('customers.billing.triggerNow', 'Trigger invoice now')}
</Button>
<p className="text-xs text-muted-theme mt-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{form.billingCadence === 'manual'
? t('customers.billing.triggerHintManual',
'Issues the running draft immediately. Manual-cadence drafts never ship automatically — this is the only way to send them. Refuses when nothing has been queued.')
@@ -899,13 +899,13 @@ export const CustomerDetailPage: React.FC = () => {
firing the standard portal-invitation email. We show ONE
card with the right action based on the customer's state. */}
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-1 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-1 flex items-center gap-2">
<KeyRound className="w-5 h-5" />
{t('customers.detail.passwordSection', 'Account actions')}
</h2>
{customer.isPassive ? (
<>
<p className="text-xs text-muted-theme mb-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
{t(
'customers.passive.detailHint',
'This customer has no portal access (admin-only record). Click below to email them a portal sign-up link. The customer\'s existing invoices, quotes, and gallery assignments are preserved when they claim the invitation.',
@@ -921,7 +921,7 @@ export const CustomerDetailPage: React.FC = () => {
{t('customers.passive.sendInvite', 'Send portal invitation')}
</Button>
{!customer.isActive && (
<p className="text-xs text-muted-theme mt-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{t('customers.passive.deactivatedHint',
'Reactivate the customer before sending the invitation.')}
</p>
@@ -929,7 +929,7 @@ export const CustomerDetailPage: React.FC = () => {
</>
) : (
<>
<p className="text-xs text-muted-theme mb-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
{t(
'customers.detail.passwordHint',
'Sends a 7-day single-use reset link to the customer\'s email. The customer\'s current password keeps working until they click the link and set a new one.'
@@ -945,7 +945,7 @@ export const CustomerDetailPage: React.FC = () => {
{t('customers.detail.passwordReset.button', 'Send password reset email')}
</Button>
{!customer.isActive && (
<p className="text-xs text-muted-theme mt-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{t('customers.detail.passwordReset.inactive', 'Reactivate the customer before sending a reset.')}
</p>
)}
@@ -1002,15 +1002,15 @@ export const CustomerDetailPage: React.FC = () => {
{confirmDeactivate && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div className="w-full max-w-md rounded-xl shadow-lg" style={{ backgroundColor: 'var(--color-surface)' }}>
<div className="w-full max-w-md rounded-xl shadow-lg bg-white dark:bg-neutral-900">
<div className="p-6">
<div className="flex items-start gap-3 mb-4">
<AlertTriangle className="w-5 h-5 mt-0.5 text-amber-500" />
<div>
<h2 className="text-lg font-semibold text-theme">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{t('customers.deactivate.title', 'Deactivate customer?')}
</h2>
<p className="mt-1 text-sm text-muted-theme">
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.deactivate.body',
'They will no longer be able to log in. You can re-activate or fully erase them later.')}
</p>
@@ -1039,15 +1039,15 @@ export const CustomerDetailPage: React.FC = () => {
and audit-log references are preserved. */}
{confirmErase && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div className="w-full max-w-md rounded-xl shadow-lg" style={{ backgroundColor: 'var(--color-surface)' }}>
<div className="w-full max-w-md rounded-xl shadow-lg bg-white dark:bg-neutral-900">
<div className="p-6">
<div className="flex items-start gap-3 mb-4">
<AlertTriangle className="w-5 h-5 mt-0.5 text-red-600" />
<div>
<h2 className="text-lg font-semibold text-theme">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{t('customers.erase.title', 'Erase customer data?')}
</h2>
<p className="mt-1 text-sm text-muted-theme">
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.erase.body',
'Removes the customer\'s name, email, phone, address, company and credentials. The account row stays so historical event-access records and audit logs still reference it. This is irreversible — you cannot restore the data afterwards.')}
</p>
@@ -118,16 +118,16 @@ export const CustomerManagementPage: React.FC = () => {
const display = c.displayName?.trim()
|| [c.firstName, c.lastName].filter(Boolean).join(' ').trim()
|| c.companyName?.trim();
return display || <span className="text-muted-theme italic">{t('customers.unnamed', 'Unnamed')}</span>;
return display || <span className="text-neutral-500 dark:text-neutral-400 italic">{t('customers.unnamed', 'Unnamed')}</span>;
};
const renderTabs = () => (
<div className="flex gap-6 border-b mb-6" style={{ borderColor: 'var(--color-surface-border)' }}>
<div className="flex gap-6 border-b border-neutral-200 dark:border-neutral-700 mb-6">
<button
type="button"
onClick={() => setActiveTab('customers')}
className={`pb-3 -mb-px border-b-2 text-sm font-medium ${
activeTab === 'customers' ? 'border-accent text-accent' : 'border-transparent text-muted-theme hover:text-theme'
activeTab === 'customers' ? 'border-accent text-accent' : 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100'
}`}
>
{t('customers.tabs.customers', 'Customers')}
@@ -137,7 +137,7 @@ export const CustomerManagementPage: React.FC = () => {
type="button"
onClick={() => setActiveTab('invitations')}
className={`pb-3 -mb-px border-b-2 text-sm font-medium ${
activeTab === 'invitations' ? 'border-accent text-accent' : 'border-transparent text-muted-theme hover:text-theme'
activeTab === 'invitations' ? 'border-accent text-accent' : 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100'
}`}
>
{t('customers.tabs.invitations', 'Invitations')}
@@ -151,7 +151,7 @@ export const CustomerManagementPage: React.FC = () => {
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-theme">{t('customers.pageTitle', 'Customers')}</h1>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('customers.pageTitle', 'Customers')}</h1>
{/* Beta badge — Calendar/Quotes/Bills tabs in the customer
surface are placeholders, so flag the whole feature as
still evolving. Keeps expectations honest. */}
@@ -162,7 +162,7 @@ export const CustomerManagementPage: React.FC = () => {
{t('navigation.betaTag', 'Beta')}
</span>
</div>
<p className="text-sm text-muted-theme mt-1">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
{t('customers.pageSubtitle', 'Recurring customer accounts that can log in at /customer/login.')}
</p>
</div>
@@ -197,14 +197,14 @@ export const CustomerManagementPage: React.FC = () => {
{t('customers.loadError', 'Could not load customers')}
</div>
) : filteredCustomers.length === 0 ? (
<div className="text-center text-muted-theme py-12">
<div className="text-center text-neutral-500 dark:text-neutral-400 py-12">
{t('customers.empty', 'No customers yet. Click "Invite customer" to add one.')}
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="text-left text-muted-theme">
<tr className="text-left text-neutral-500 dark:text-neutral-400">
<th className="px-3 py-2 font-medium">{t('customers.table.name', 'Name')}</th>
<th className="px-3 py-2 font-medium">{t('customers.table.email', 'Email')}</th>
<th className="px-3 py-2 font-medium">{t('customers.table.company', 'Company')}</th>
@@ -216,16 +216,16 @@ export const CustomerManagementPage: React.FC = () => {
</thead>
<tbody>
{filteredCustomers.map((c) => (
<tr key={c.id} className="border-t" style={{ borderColor: 'var(--color-surface-border)' }}>
<tr key={c.id} className="border-t border-neutral-200 dark:border-neutral-700">
<td className="px-3 py-3">
<Link to={`/admin/clients/accounts/${c.id}`} className="text-theme hover:underline">
<Link to={`/admin/clients/accounts/${c.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline">
{renderCustomerName(c)}
</Link>
</td>
<td className="px-3 py-3 text-muted-theme">{c.email}</td>
<td className="px-3 py-3 text-muted-theme">{c.companyName || '—'}</td>
<td className="px-3 py-3 text-muted-theme">{c.eventCount ?? 0}</td>
<td className="px-3 py-3 text-muted-theme">{formatDate(c.lastLogin)}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">{c.email}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">{c.companyName || '—'}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">{c.eventCount ?? 0}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">{formatDate(c.lastLogin)}</td>
<td className="px-3 py-3">
<div className="flex flex-col gap-1">
{c.isActive ? (
@@ -278,14 +278,14 @@ export const CustomerManagementPage: React.FC = () => {
{t('customers.loadInvitationsError', 'Could not load invitations')}
</div>
) : filteredInvitations.length === 0 ? (
<div className="text-center text-muted-theme py-12">
<div className="text-center text-neutral-500 dark:text-neutral-400 py-12">
{t('customers.invitations.empty', 'No pending invitations.')}
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="text-left text-muted-theme">
<tr className="text-left text-neutral-500 dark:text-neutral-400">
<th className="px-3 py-2 font-medium">{t('customers.invitations.email', 'Email')}</th>
<th className="px-3 py-2 font-medium">{t('customers.invitations.invitedBy', 'Invited by')}</th>
<th className="px-3 py-2 font-medium">{t('customers.invitations.expiresAt', 'Expires')}</th>
@@ -295,16 +295,16 @@ export const CustomerManagementPage: React.FC = () => {
</thead>
<tbody>
{filteredInvitations.map((inv: CustomerInvitationSummary) => (
<tr key={inv.id} className="border-t" style={{ borderColor: 'var(--color-surface-border)' }}>
<td className="px-3 py-3 text-theme">{inv.email}</td>
<td className="px-3 py-3 text-muted-theme">{inv.invitedBy || '—'}</td>
<td className="px-3 py-3 text-muted-theme">
<tr key={inv.id} className="border-t border-neutral-200 dark:border-neutral-700">
<td className="px-3 py-3 text-neutral-900 dark:text-neutral-100">{inv.email}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">{inv.invitedBy || '—'}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">
<span className="inline-flex items-center gap-1">
<Clock className="w-3.5 h-3.5" />
{formatDate(inv.expiresAt)}
</span>
</td>
<td className="px-3 py-3 text-muted-theme">{formatDate(inv.createdAt)}</td>
<td className="px-3 py-3 text-neutral-500 dark:text-neutral-400">{formatDate(inv.createdAt)}</td>
<td className="px-3 py-3 text-right">
<Button
type="button"
@@ -338,8 +338,7 @@ export const CustomerManagementPage: React.FC = () => {
onClick={() => setCreateMode(null)}
>
<div
className="w-full max-w-2xl rounded-xl shadow-lg max-h-[90vh] overflow-y-auto"
style={{ backgroundColor: 'var(--color-surface)' }}
className="w-full max-w-2xl rounded-xl shadow-lg max-h-[90vh] overflow-y-auto bg-white dark:bg-neutral-900"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
@@ -359,17 +358,17 @@ export const CustomerManagementPage: React.FC = () => {
{confirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div className="w-full max-w-md rounded-xl shadow-lg" style={{ backgroundColor: 'var(--color-surface)' }}>
<div className="w-full max-w-md rounded-xl shadow-lg bg-white dark:bg-neutral-900">
<div className="p-6">
<div className="flex items-start gap-3 mb-4">
<AlertTriangle className="w-5 h-5 mt-0.5 text-amber-500" />
<div>
<h2 className="text-lg font-semibold text-theme">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{confirm.kind === 'deactivate'
? t('customers.deactivate.title', 'Deactivate customer?')
: t('customers.cancelInvitation.title', 'Cancel invitation?')}
</h2>
<p className="mt-1 text-sm text-muted-theme">
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
{confirm.kind === 'deactivate'
? t('customers.deactivate.body',
'They will no longer be able to log in. You can re-invite them later.')
@@ -394,7 +394,7 @@ export const BillDetailPage: React.FC = () => {
<div><div className="text-neutral-600 dark:text-neutral-300">{t('bills.field.eventName', 'Event')}</div>
<div>
{inv.eventId ? (
<Link to={`/admin/events/${inv.eventId}`} className="text-theme hover:underline">{inv.eventName}</Link>
<Link to={`/admin/events/${inv.eventId}`} className="text-neutral-900 dark:text-neutral-100 hover:underline">{inv.eventName}</Link>
) : inv.eventName}
{inv.eventDate ? ` · ${fmtDate(inv.eventDate)}` : ''}
</div>
@@ -61,7 +61,7 @@ export const BillsListPage: React.FC = () => {
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-theme">{t('bills.title', 'Invoices')}</h1>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('bills.title', 'Invoices')}</h1>
{/* Beta badge — matches the Customers + Quotes pages. */}
<span
className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded font-semibold bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300"
@@ -70,7 +70,7 @@ export const BillsListPage: React.FC = () => {
{t('navigation.betaTag', 'Beta')}
</span>
</div>
<p className="text-sm text-muted-theme mt-1">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
{t('bills.subtitle', 'Schedule, send, track payments and chase late invoices.')}
</p>
</div>
@@ -122,7 +122,7 @@ export const BillsListPage: React.FC = () => {
{/* Body inside the same card (matches Customers + Quotes). */}
<div className="mt-4">
{isLoading ? <Loading /> : !data || data.invoices.length === 0 ? (
<p className="text-center text-muted-theme py-8">{t('bills.empty', 'No invoices yet.')}</p>
<p className="text-center text-neutral-500 dark:text-neutral-400 py-8">{t('bills.empty', 'No invoices yet.')}</p>
) : (
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 overflow-hidden">
<div className="overflow-x-auto">
@@ -173,11 +173,11 @@ export const BillsListPage: React.FC = () => {
<td className="px-3 py-2 truncate max-w-xs">
{inv.eventName
? (inv.eventId
? <Link to={`/admin/events/${inv.eventId}`} className="text-theme hover:underline" onClick={(e) => e.stopPropagation()}>{inv.eventName}</Link>
? <Link to={`/admin/events/${inv.eventId}`} className="text-neutral-900 dark:text-neutral-100 hover:underline" onClick={(e) => e.stopPropagation()}>{inv.eventName}</Link>
: inv.eventName)
: '—'}
</td>
<td className="px-3 py-2 text-xs text-muted-theme">
<td className="px-3 py-2 text-xs text-neutral-500 dark:text-neutral-400">
{inv.installmentTotal > 1 ? `${inv.installmentIndex + 1}/${inv.installmentTotal} · ${inv.installmentLabel || ''}` : '—'}
</td>
<td className="px-3 py-2 whitespace-nowrap">{inv.issueDate ? fmtDate(inv.issueDate) : '—'}</td>
@@ -436,10 +436,10 @@ export const CalendarPage: React.FC = () => {
<div className="space-y-4">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h1 className="text-2xl font-bold text-theme">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{t('calendar.pageTitle', 'Calendar')}
</h1>
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('calendar.subtitle',
'Events, logged hours, and pending quotes/contracts in one view.')}
</p>
@@ -466,7 +466,7 @@ export const CalendarPage: React.FC = () => {
<Card padding="md">
{itemsLoading && !itemsResp && (
<div className="mb-3 flex items-center gap-2 text-sm text-muted-theme">
<div className="mb-3 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
<Loading />
<span>{t('calendar.loading', 'Loading items…')}</span>
</div>
@@ -609,6 +609,11 @@ export const CalendarPage: React.FC = () => {
and the active state (selected view) all read from these
vars so the calendar respects custom branding palettes. */}
<style>{`
/* Admin-toggle-aware chrome vars — gallery theme writes inline
--color-* on :root which would otherwise override .dark; scope
local vars so the calendar follows the admin light/dark toggle. */
.fc { --cal-border: #e5e5e5; --cal-muted: #737373; --cal-text: #171717; }
.dark .fc { --cal-border: #262626; --cal-muted: #a3a3a3; --cal-text: #f5f5f5; }
.cal-dashed {
border-style: dashed !important;
background-color: transparent !important;
@@ -624,8 +629,8 @@ export const CalendarPage: React.FC = () => {
/* FC button restyle — match picpeak admin .btn-outline shape. */
.fc .fc-button-primary {
background-color: transparent;
border: 1px solid var(--color-surface-border);
color: var(--color-muted-text);
border: 1px solid var(--cal-border);
color: var(--cal-muted);
text-transform: none;
font-weight: 500;
box-shadow: none;
@@ -634,8 +639,8 @@ export const CalendarPage: React.FC = () => {
.fc .fc-button-primary:hover:not(:disabled) {
opacity: 0.8;
background-color: transparent;
border-color: var(--color-surface-border);
color: var(--color-muted-text);
border-color: var(--cal-border);
color: var(--cal-muted);
}
.fc .fc-button-primary:focus,
.fc .fc-button-primary:focus-visible {
@@ -652,14 +657,14 @@ export const CalendarPage: React.FC = () => {
.fc .fc-button-primary:disabled {
opacity: 0.5;
background-color: transparent;
border-color: var(--color-surface-border);
color: var(--color-muted-text);
border-color: var(--cal-border);
color: var(--cal-muted);
}
/* Toolbar title (month / week range) reads the regular text
colour rather than FC's hardcoded #333 so dark mode looks
right. */
.fc .fc-toolbar-title {
color: var(--color-text);
color: var(--cal-text);
font-size: 1.125rem;
font-weight: 600;
}
@@ -677,12 +682,12 @@ export const CalendarPage: React.FC = () => {
selector that maps to a day column so the rule works
regardless of which DOM shape FC ends up rendering. */
.fc {
--fc-border-color: var(--color-surface-border);
--fc-border-color: var(--cal-border);
}
.fc .fc-timegrid-col:not(:first-of-type),
.fc .fc-day:not(:first-child),
.fc .fc-timegrid-cols > table > tbody > tr > td:not(:first-child) {
box-shadow: inset 1px 0 0 var(--color-surface-border);
box-shadow: inset 1px 0 0 var(--cal-border);
}
`}</style>
</div>
@@ -698,7 +703,7 @@ const Legend: React.FC = () => {
const { t } = useTranslation();
return (
<Card padding="sm">
<div className="flex flex-wrap gap-4 text-xs text-muted-theme">
<div className="flex flex-wrap gap-4 text-xs text-neutral-500 dark:text-neutral-400">
<LegendSwatch color={COLOR_EVENT} label={t('calendar.legend.events', 'Events')} />
<LegendSwatch color={COLOR_HOURS} label={t('calendar.legend.hours', 'Hours')} />
<LegendSwatch
@@ -239,14 +239,14 @@ export const CrmDevelopmentPage: React.FC = () => {
<MailCheck className="w-4 h-4" />
{t('crmDev.paymentCheck.title', 'Test payment-check email (real invoice)')}
</h3>
<p className="text-sm text-muted-theme mb-4">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
{t('crmDev.paymentCheck.help',
'Fires the admin payment-check email for a real sent/overdue invoice, bypassing the 24h throttle. The three buttons in the email are real signed tokens — clicking them will affect the invoice status.')}
</p>
{invoiceListLoading ? <Loading /> : (
<>
<label className="block text-xs uppercase tracking-wider text-muted-theme mb-1">
<label className="block text-xs uppercase tracking-wider text-neutral-500 dark:text-neutral-400 mb-1">
{t('crmDev.paymentCheck.selectInvoice', 'Sent or overdue invoice')}
</label>
<select
@@ -262,7 +262,7 @@ export const CrmDevelopmentPage: React.FC = () => {
))}
</select>
{invoiceList && invoiceList.invoices.length === 0 && (
<p className="text-sm text-muted-theme mb-3">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-3">
{t('crmDev.paymentCheck.noneAvailable',
'No sent or overdue invoices in the database.')}
</p>
@@ -285,7 +285,7 @@ export const CrmDevelopmentPage: React.FC = () => {
<Mail className="w-4 h-4" />
{t('crmDev.templates.title', 'Send any CRM email to me (mock data)')}
</h3>
<p className="text-sm text-muted-theme mb-4">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
{t('crmDev.templates.help',
'Queues the chosen template to your own admin email with placeholder values. When the install has a real quote / invoice on file, the appropriate PDF is attached so you can verify the full output.')}
</p>
@@ -294,12 +294,12 @@ export const CrmDevelopmentPage: React.FC = () => {
// Env gate banner above already explains the situation —
// keep this card empty rather than rendering a misleading
// "0 templates" list.
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('crmDev.envDisabled.cardHint',
'Email templates can\'t be listed until the dev-tools env gate is opened.')}
</p>
) : (templates && templates.length === 0) ? (
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('crmDev.templates.empty',
'No CRM email templates found — run migrations to seed them.')}
</p>
@@ -323,7 +323,7 @@ export const CrmDevelopmentPage: React.FC = () => {
</div>
<div className="text-sm font-medium mt-0.5">{title}</div>
{description && (
<div className="text-xs text-muted-theme mt-0.5">{description}</div>
<div className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">{description}</div>
)}
</div>
<Button
@@ -238,7 +238,7 @@ export const HourEntryDragCreateModal: React.FC<HourEntryDragCreateModalProps> =
<h2 className="font-semibold text-lg mb-1">
{t('calendar.hourEntry.createTitle', 'Log hours')}
</h2>
<p className="text-xs text-muted-theme mb-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
{/* The pre-filled range is part of the page state, not editable
from this modal. Admin can edit start/end after creating
via the inline-edit popover (also in this commit). */}
@@ -144,7 +144,7 @@ export const HourEntryInlinePopover: React.FC<HourEntryInlinePopoverProps> = ({
<h2 className="font-semibold text-lg">
{item.customerName || t('calendar.hourEntry.untitledCustomer', 'Hours')}
</h2>
<p className="text-xs text-muted-theme">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{item.entryDate} · {item.startTime}{item.endTime}
</p>
</div>
@@ -166,7 +166,7 @@ export const HourEntryInlinePopover: React.FC<HourEntryInlinePopoverProps> = ({
// here so the admin can't accidentally type into a locked
// entry. The invoice link is omitted for now — clicking
// through to the bill belongs on a follow-up commit.
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{item.description || t('calendar.hourEntry.noDescription', 'No description.')}
</p>
) : (
@@ -91,7 +91,7 @@ export const HoursLoggingPage: React.FC = () => {
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-theme">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{t('hoursLogging.title', 'Hours logging')}
</h1>
{/* Beta badge — matches Customers + Quotes + Contracts +
@@ -104,7 +104,7 @@ export const HoursLoggingPage: React.FC = () => {
{t('navigation.betaTag', 'Beta')}
</span>
</div>
<p className="text-sm text-muted-theme mt-1">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
{t('hoursLogging.subtitle',
'Pick a customer and log billable time blocks. Entries flow into the next monthly bill or are billed on demand for per-event customers.')}
</p>
@@ -112,7 +112,7 @@ export const HoursLoggingPage: React.FC = () => {
</div>
<Card padding="lg">
<label className="block text-sm font-medium text-theme mb-2">
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-2">
{t('hoursLogging.pickCustomer', 'Customer')}
</label>
<CustomerPicker
@@ -158,22 +158,22 @@ export const HoursLoggingPage: React.FC = () => {
{!selectedId && (
<Card padding="lg">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-4 h-4 text-muted-theme" />
<h2 className="text-base font-semibold text-theme">
<Clock className="w-4 h-4 text-neutral-500 dark:text-neutral-400" />
<h2 className="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{t('hoursLogging.openHours.title', 'Open hours across all customers')}
</h2>
</div>
<p className="text-sm text-muted-theme mb-4">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
{t('hoursLogging.openHours.subtitle',
'Unbilled time blocks waiting to be billed. Pick a customer above, or click a row to drill in.')}
</p>
{summaryLoading ? (
<p className="text-sm text-muted-theme py-6 text-center">
<p className="text-sm text-neutral-500 dark:text-neutral-400 py-6 text-center">
{t('common.loading', 'Loading…')}
</p>
) : summary.length === 0 ? (
<p className="text-sm text-muted-theme py-6 text-center">
<p className="text-sm text-neutral-500 dark:text-neutral-400 py-6 text-center">
{t('hoursLogging.openHours.empty',
'No unbilled hours right now — everything is billed or no time has been logged yet.')}
</p>
@@ -188,14 +188,14 @@ export const HoursLoggingPage: React.FC = () => {
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-theme truncate">{summaryLabel(r)}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100 truncate">{summaryLabel(r)}</span>
{r.isPassive && (
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-600 dark:bg-neutral-700 dark:text-neutral-300">
{t('hoursLogging.openHours.passive', 'Passive')}
</span>
)}
</div>
<div className="text-xs text-muted-theme mt-0.5">
<div className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">
{t('hoursLogging.openHours.entryLine', {
count: r.entryCount,
hours: (r.totalMinutes / 60).toFixed(2),
@@ -206,7 +206,7 @@ export const HoursLoggingPage: React.FC = () => {
<div className="flex items-center gap-3 shrink-0">
<div className="text-right">
{r.rateResolvable ? (
<div className="font-semibold text-theme tabular-nums">
<div className="font-semibold text-neutral-900 dark:text-neutral-100 tabular-nums">
{formatMoneyMinor(r.openAmountMinor, currency)}
</div>
) : (
@@ -216,7 +216,7 @@ export const HoursLoggingPage: React.FC = () => {
</div>
)}
</div>
<ChevronRight className="w-4 h-4 text-muted-theme" />
<ChevronRight className="w-4 h-4 text-neutral-500 dark:text-neutral-400" />
</div>
</button>
))}
@@ -608,12 +608,12 @@ export const TaxReportPage: React.FC = () => {
colour scheme distinguishes the row kinds at
a glance across both surfaces. */}
{row.kind === 'storno' && (
<span className="ml-2 inline-block px-1.5 py-0.5 text-[10px] uppercase tracking-wider rounded bg-purple-100 text-purple-800 font-semibold not-italic">
<span className="ml-2 inline-block px-1.5 py-0.5 text-[10px] uppercase tracking-wider rounded bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-300 font-semibold not-italic">
{t('bills.kind.storno', 'Storno')}
</span>
)}
{row.isReissue && (
<span className="ml-2 inline-block px-1.5 py-0.5 text-[10px] uppercase tracking-wider rounded bg-blue-100 text-blue-800 font-semibold not-italic">
<span className="ml-2 inline-block px-1.5 py-0.5 text-[10px] uppercase tracking-wider rounded bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 font-semibold not-italic">
{t('bills.kind.reissue', 'Reissue')}
</span>
)}
@@ -243,10 +243,10 @@ export const BlockLibraryPage: React.FC = () => {
<ArrowLeft className="w-4 h-4" />
{t('contracts.blocks.back', 'Back to contracts')}
</Link>
<h1 className="text-2xl font-bold flex-1 text-theme">
<h1 className="text-2xl font-bold flex-1 text-neutral-900 dark:text-neutral-100">
{t('contracts.blocks.title', 'Contract block library')}
</h1>
<label className="flex items-center gap-2 text-sm text-muted-theme">
<label className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
<input type="checkbox" checked={hideInactive} onChange={(e) => setHideInactive(e.target.checked)} />
{t('contracts.blocks.hideInactive', 'Hide inactive')}
</label>
@@ -340,7 +340,7 @@ export const BlockLibraryPage: React.FC = () => {
);
})}
{blocks.length === 0 && (
<p className="text-center text-sm text-muted-theme py-6">
<p className="text-center text-sm text-neutral-500 dark:text-neutral-400 py-6">
{t('contracts.blocks.empty', 'No blocks yet.')}
</p>
)}
@@ -352,7 +352,7 @@ export const BlockLibraryPage: React.FC = () => {
<div className="lg:col-span-2">
{selection === null ? (
<Card padding="md">
<p className="text-center text-muted-theme py-8">
<p className="text-center text-neutral-500 dark:text-neutral-400 py-8">
{t('contracts.blocks.selectPrompt', 'Select a block on the left or create a new one to start editing.')}
</p>
</Card>
@@ -366,7 +366,7 @@ export const BlockLibraryPage: React.FC = () => {
</h3>
<div className="flex gap-2 items-center flex-wrap">
{selection.mode === 'edit' && (
<label className="flex items-center gap-2 text-sm text-muted-theme mr-2">
<label className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 mr-2">
<input
type="checkbox"
checked={selection.block.isActive}
@@ -590,7 +590,7 @@ export const ContractDetailPage: React.FC = () => {
{c.convertedEventId && (
<Card padding="md" className="mb-4">
<p className="text-sm">
<span className="text-muted-theme mr-2">
<span className="text-neutral-500 dark:text-neutral-400 mr-2">
{t('contracts.detail.convertedToEvent', 'Converted to event')}:
</span>
<Link to={`/admin/events/${c.convertedEventId}`} className="font-medium text-primary-600 dark:text-primary-400 hover:underline">
@@ -67,7 +67,7 @@ export const ContractsListPage: React.FC = () => {
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-theme">{t('contracts.title', 'Contracts')}</h1>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('contracts.title', 'Contracts')}</h1>
{/* Beta badge — matches Customers + Quotes + Invoices. */}
<span
className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded font-semibold bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300"
@@ -76,7 +76,7 @@ export const ContractsListPage: React.FC = () => {
{t('navigation.betaTag', 'Beta')}
</span>
</div>
<p className="text-sm text-muted-theme mt-1">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
{t('contracts.subtitle', 'Compose contracts from reusable blocks and have customers sign in-browser or upload a wet-signed PDF.')}
</p>
</div>
@@ -129,7 +129,7 @@ export const ContractsListPage: React.FC = () => {
single-card layout used by Customers / Quotes / Invoices. */}
<div className="mt-4">
{isLoading ? <Loading /> : !data || data.contracts.length === 0 ? (
<p className="text-center text-muted-theme py-8">{t('contracts.list.empty', 'No contracts yet.')}</p>
<p className="text-center text-neutral-500 dark:text-neutral-400 py-8">{t('contracts.list.empty', 'No contracts yet.')}</p>
) : (
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 overflow-hidden">
<div className="overflow-x-auto">
@@ -179,7 +179,7 @@ export const ContractsListPage: React.FC = () => {
</div>
{totalPages > 1 && (
<div className="flex justify-between items-center px-3 py-2 border-t border-neutral-200 dark:border-neutral-700 text-sm">
<span className="text-muted-theme">
<span className="text-neutral-500 dark:text-neutral-400">
{t('contracts.list.pagination', 'Page {{page}} of {{total}} · {{count}} contracts', {
page, total: totalPages, count: data.total,
})}
@@ -53,7 +53,7 @@ export const QuotesListPage: React.FC = () => {
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-theme">{t('quotes.title', 'Quotes')}</h1>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('quotes.title', 'Quotes')}</h1>
{/* Beta badge feature is functional but the surface is
still evolving (matches Customers + Invoices). */}
<span
@@ -63,7 +63,7 @@ export const QuotesListPage: React.FC = () => {
{t('navigation.betaTag', 'Beta')}
</span>
</div>
<p className="text-sm text-muted-theme mt-1">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
{t('quotes.subtitle', 'Send, track and convert quotes into events.')}
</p>
</div>
@@ -104,7 +104,7 @@ export const QuotesListPage: React.FC = () => {
single-card layout used by Customers/Invitations. */}
<div className="mt-4">
{isLoading ? <Loading /> : !data || data.quotes.length === 0 ? (
<p className="text-center text-muted-theme py-8">{t('quotes.empty', 'No quotes yet.')}</p>
<p className="text-center text-neutral-500 dark:text-neutral-400 py-8">{t('quotes.empty', 'No quotes yet.')}</p>
) : (
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 overflow-hidden">
<div className="overflow-x-auto">
@@ -147,7 +147,7 @@ export const QuotesListPage: React.FC = () => {
</div>
{data.pagination.totalPages > 1 && (
<div className="flex justify-between items-center px-3 py-2 border-t border-neutral-200 dark:border-neutral-700 text-sm">
<span className="text-muted-theme">
<span className="text-neutral-500 dark:text-neutral-400">
{t('quotes.pagination', 'Page {{page}} of {{total}} · {{count}} quotes', {
page: data.pagination.page, total: data.pagination.totalPages, count: data.pagination.total,
})}
@@ -177,7 +177,7 @@ export const CrmSettingsPage: React.FC = () => {
{!anySection && (
<Card>
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('crmSettings.emptyState',
'No CRM features are currently enabled. Turn on Quotes, Invoices, or Contracts in Settings → Features to see the matching configuration here.')}
</p>
@@ -349,8 +349,8 @@ const PdfToggleRow: React.FC<PdfToggleRowProps> = ({ label, description, enabled
role="switch"
aria-checked={enabled}
onClick={() => onChange(!enabled)}
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 shrink-0"
style={{ backgroundColor: enabled ? 'var(--color-accent)' : 'var(--color-surface-border)' }}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 shrink-0 ${enabled ? '' : 'bg-neutral-300 dark:bg-neutral-600'}`}
style={enabled ? { backgroundColor: 'var(--color-accent)' } : undefined}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`}