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:
@@ -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>
|
||||
|
||||
@@ -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 customer’s 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'}`}
|
||||
|
||||
Reference in New Issue
Block a user