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