diff --git a/app/Enums/ChartColorScheme.php b/app/Enums/ChartColorScheme.php new file mode 100644 index 00000000..3506d759 --- /dev/null +++ b/app/Enums/ChartColorScheme.php @@ -0,0 +1,11 @@ +user()->setting()->updateOrCreate( + ['user_id' => $request->user()->id], + ['chart_color_scheme' => $request->validated('chart_color_scheme')] + ); + + return back(); + } +} diff --git a/app/Http/Middleware/HandleAppearance.php b/app/Http/Middleware/HandleAppearance.php index f1a02bbc..d974fd62 100644 --- a/app/Http/Middleware/HandleAppearance.php +++ b/app/Http/Middleware/HandleAppearance.php @@ -17,6 +17,7 @@ class HandleAppearance public function handle(Request $request, Closure $next): Response { View::share('appearance', $request->cookie('appearance') ?? 'system'); + View::share('chartColorScheme', $request->cookie('chart-color-scheme') ?? 'colorful'); return $next($request); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 7a1723e0..7dc3e9c3 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -83,6 +83,7 @@ class HandleInertiaRequests extends Middleware 'bestValuePlan' => config('subscriptions.best_value_plan', null), 'promo' => config('subscriptions.promo', []), ], + 'chartColorScheme' => $user?->setting?->chart_color_scheme?->value ?? 'colorful', 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'features' => [ 'cashflow' => true, diff --git a/app/Http/Requests/Settings/UpdateChartColorSchemeRequest.php b/app/Http/Requests/Settings/UpdateChartColorSchemeRequest.php new file mode 100644 index 00000000..9e7d2043 --- /dev/null +++ b/app/Http/Requests/Settings/UpdateChartColorSchemeRequest.php @@ -0,0 +1,22 @@ +|string> + */ + public function rules(): array + { + return [ + 'chart_color_scheme' => ['required', 'string', Rule::enum(ChartColorScheme::class)], + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 4e715ba9..caef4dfd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -69,6 +69,11 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma return $this->onboarded_at !== null; } + public function setting(): HasOne + { + return $this->hasOne(UserSetting::class); + } + public function encryptedMessage(): HasOne { return $this->hasOne(EncryptedMessage::class); diff --git a/app/Models/UserSetting.php b/app/Models/UserSetting.php new file mode 100644 index 00000000..4d335e16 --- /dev/null +++ b/app/Models/UserSetting.php @@ -0,0 +1,32 @@ + */ + use HasFactory, HasUuids; + + protected $fillable = [ + 'user_id', + 'chart_color_scheme', + ]; + + protected function casts(): array + { + return [ + 'chart_color_scheme' => ChartColorScheme::class, + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index f16a03a5..756e2e08 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -19,7 +19,7 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - $middleware->encryptCookies(except: ['appearance', 'sidebar_state']); + $middleware->encryptCookies(except: ['appearance', 'sidebar_state', 'chart-color-scheme']); $middleware->trustProxies( at: '*', diff --git a/database/factories/UserSettingFactory.php b/database/factories/UserSettingFactory.php new file mode 100644 index 00000000..6deb02fb --- /dev/null +++ b/database/factories/UserSettingFactory.php @@ -0,0 +1,33 @@ + + */ +class UserSettingFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'chart_color_scheme' => ChartColorScheme::Colorful, + ]; + } + + public function withScheme(ChartColorScheme $scheme): static + { + return $this->state(fn (array $attributes) => [ + 'chart_color_scheme' => $scheme, + ]); + } +} diff --git a/database/migrations/2026_02_08_103953_create_user_settings_table.php b/database/migrations/2026_02_08_103953_create_user_settings_table.php new file mode 100644 index 00000000..8be747cd --- /dev/null +++ b/database/migrations/2026_02_08_103953_create_user_settings_table.php @@ -0,0 +1,29 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->unique()->constrained()->cascadeOnDelete(); + $table->string('chart_color_scheme')->default('colorful'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_settings'); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index 15072b54..6c48d4a9 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -157,6 +157,111 @@ --sidebar-ring: oklch(0.439 0 0); } +/* Colorful chart color scheme */ +[data-chart-color='colorful'] { + --chart-1: var(--color-blue-600); + --chart-2: var(--color-emerald-500); + --chart-3: var(--color-amber-500); + --chart-4: var(--color-rose-500); + --chart-5: var(--color-violet-500); + --chart-6: var(--color-cyan-500); + --chart-7: var(--color-orange-500); + --chart-8: var(--color-teal-500); + --chart-9: var(--color-pink-400); + --chart-10: var(--color-indigo-400); + --spent: var(--color-blue-600); + --allocated: var(--color-blue-200); + --spent-prev: var(--color-blue-400); + --account-line: var(--color-zinc-800); + --cashflow-income: var(--color-emerald-500); + --cashflow-expense: var(--color-red-500); +} + +.dark[data-chart-color='colorful'] { + --chart-1: var(--color-blue-400); + --chart-2: var(--color-emerald-400); + --chart-3: var(--color-amber-400); + --chart-4: var(--color-rose-400); + --chart-5: var(--color-violet-400); + --chart-6: var(--color-cyan-400); + --chart-7: var(--color-orange-400); + --chart-8: var(--color-teal-400); + --chart-9: var(--color-pink-300); + --chart-10: var(--color-indigo-300); + --spent: var(--color-blue-400); + --allocated: var(--color-blue-800); + --spent-prev: var(--color-blue-600); + --account-line: var(--color-zinc-50); + --cashflow-income: var(--color-emerald-400); + --cashflow-expense: var(--color-red-400); +} + +/* Blue chart color scheme */ +[data-chart-color='blue'] { + --chart-1: var(--color-blue-900); + --chart-2: var(--color-blue-800); + --chart-3: var(--color-blue-700); + --chart-4: var(--color-blue-600); + --chart-5: var(--color-blue-500); + --chart-6: var(--color-blue-400); + --chart-7: var(--color-blue-300); + --chart-8: var(--color-blue-200); + --chart-9: var(--color-blue-100); + --chart-10: var(--color-blue-50); + --spent: var(--color-blue-800); + --allocated: var(--color-blue-200); + --spent-prev: var(--color-blue-400); +} + +.dark[data-chart-color='blue'] { + --chart-1: var(--color-blue-200); + --chart-2: var(--color-blue-300); + --chart-3: var(--color-blue-400); + --chart-4: var(--color-blue-500); + --chart-5: var(--color-blue-600); + --chart-6: var(--color-blue-700); + --chart-7: var(--color-blue-800); + --chart-8: var(--color-blue-900); + --chart-9: var(--color-blue-50); + --chart-10: var(--color-blue-100); + --spent: var(--color-blue-200); + --allocated: var(--color-blue-700); + --spent-prev: var(--color-blue-500); +} + +/* Pink chart color scheme */ +[data-chart-color='pink'] { + --chart-1: var(--color-pink-900); + --chart-2: var(--color-pink-800); + --chart-3: var(--color-pink-700); + --chart-4: var(--color-pink-600); + --chart-5: var(--color-pink-500); + --chart-6: var(--color-pink-400); + --chart-7: var(--color-pink-300); + --chart-8: var(--color-pink-200); + --chart-9: var(--color-pink-100); + --chart-10: var(--color-pink-50); + --spent: var(--color-pink-800); + --allocated: var(--color-pink-200); + --spent-prev: var(--color-pink-400); +} + +.dark[data-chart-color='pink'] { + --chart-1: var(--color-pink-200); + --chart-2: var(--color-pink-300); + --chart-3: var(--color-pink-400); + --chart-4: var(--color-pink-500); + --chart-5: var(--color-pink-600); + --chart-6: var(--color-pink-700); + --chart-7: var(--color-pink-800); + --chart-8: var(--color-pink-900); + --chart-9: var(--color-pink-50); + --chart-10: var(--color-pink-100); + --spent: var(--color-pink-200); + --allocated: var(--color-pink-700); + --spent-prev: var(--color-pink-500); +} + @layer base { * { @apply border-border; diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 1417bb16..a431496c 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -17,6 +17,7 @@ import { EncryptionKeyProvider } from './contexts/encryption-key-context'; import { PrivacyModeProvider } from './contexts/privacy-mode-context'; import { SyncProvider } from './contexts/sync-context'; import { initializeTheme } from './hooks/use-appearance'; +import { initializeChartColorScheme } from './hooks/use-chart-color-scheme'; import { initializePostHog } from './lib/posthog'; import type { SharedData } from './types'; import { setTranslations } from './utils/i18n'; @@ -33,6 +34,7 @@ initializePostHog(); // Initialize theme before creating the app so progress bar color is correct initializeTheme(); +initializeChartColorScheme(); const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; diff --git a/resources/js/components/accounts/account-list-card.tsx b/resources/js/components/accounts/account-list-card.tsx index 72c00d1b..688800c4 100644 --- a/resources/js/components/accounts/account-list-card.tsx +++ b/resources/js/components/accounts/account-list-card.tsx @@ -4,6 +4,7 @@ import { BankLogo } from '@/components/bank-logo'; import { AmountTrendIndicator } from '@/components/dashboard/amount-trend-indicator'; import { AmountDisplay } from '@/components/ui/amount-display'; import { Card, CardContent } from '@/components/ui/card'; +import { useChartColors } from '@/hooks/use-chart-color-scheme'; import { AccountWithMetrics } from '@/hooks/use-dashboard-data'; import { supportsInvestedAmount } from '@/types/account'; import { __ } from '@/utils/i18n'; @@ -24,6 +25,7 @@ export function AccountListCard({ loading, onBalanceUpdated, }: AccountListCardProps) { + const { accountMainLineColor, accountGainLineColor } = useChartColors(); const [updateBalanceOpen, setUpdateBalanceOpen] = useState(false); if (loading) { @@ -225,7 +227,7 @@ export function AccountListCard({ @@ -233,7 +235,7 @@ export function AccountListCard({ diff --git a/resources/js/components/charts/cashflow-trend-chart.tsx b/resources/js/components/charts/cashflow-trend-chart.tsx index 36678985..701421ad 100644 --- a/resources/js/components/charts/cashflow-trend-chart.tsx +++ b/resources/js/components/charts/cashflow-trend-chart.tsx @@ -8,6 +8,7 @@ import { } from '@/components/ui/card'; import { ChartConfig, ChartContainer } from '@/components/ui/chart'; import { TrendDataPoint } from '@/hooks/use-cashflow-data'; +import { useChartColors } from '@/hooks/use-chart-color-scheme'; import { useLocale } from '@/hooks/use-locale'; import { cn } from '@/lib/utils'; import { formatCompactNumber, formatMonthFromYearMonth } from '@/utils/date'; @@ -43,12 +44,16 @@ interface CustomTooltipProps { active?: boolean; payload?: TooltipPayloadItem[]; currency?: string; + incomeColor?: string; + expenseColor?: string; } function CustomTooltip({ active, payload, currency = 'USD', + incomeColor, + expenseColor, }: CustomTooltipProps) { const locale = useLocale(); @@ -65,7 +70,10 @@ function CustomTooltip({
- + {__('Income')}
- + {__('Expenses')} (null); const minChartWidth = data.length * 60; + const { cashflowIncomeColor, cashflowExpenseColor } = useChartColors(); const chartConfig: ChartConfig = { income: { label: __('Income'), - color: 'var(--color-chart-2)', + color: cashflowIncomeColor, }, expense: { label: __('Expenses'), - color: 'var(--color-chart-5)', + color: cashflowExpenseColor, }, net: { label: __('Net'), @@ -214,7 +226,13 @@ export function CashflowTrendChart({ /> } + content={ + + } cursor={{ fill: 'var(--color-muted)', opacity: 0.3, @@ -223,7 +241,7 @@ export function CashflowTrendChart({
- + {__('Income')}
- + {__('Expenses')}
diff --git a/resources/js/components/dashboard/account-balance-card.tsx b/resources/js/components/dashboard/account-balance-card.tsx index 1f16c91f..fdb311de 100644 --- a/resources/js/components/dashboard/account-balance-card.tsx +++ b/resources/js/components/dashboard/account-balance-card.tsx @@ -4,6 +4,7 @@ import { UpdateBalanceDialog } from '@/components/accounts/update-balance-dialog import { BankLogo } from '@/components/bank-logo'; import { AmountDisplay } from '@/components/ui/amount-display'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { useChartColors } from '@/hooks/use-chart-color-scheme'; import { AccountWithMetrics } from '@/hooks/use-dashboard-data'; import { supportsInvestedAmount } from '@/types/account'; import { __ } from '@/utils/i18n'; @@ -24,6 +25,7 @@ export function AccountBalanceCard({ loading, onBalanceUpdated, }: AccountBalanceCardProps) { + const { accountMainLineColor, accountGainLineColor } = useChartColors(); const [updateBalanceOpen, setUpdateBalanceOpen] = useState(false); if (loading) { return ( @@ -210,7 +212,7 @@ export function AccountBalanceCard({ @@ -218,7 +220,7 @@ export function AccountBalanceCard({ ().props; + const { categoryBarColor } = useChartColors(); if (loading || !auth?.user) { return ( @@ -97,8 +86,10 @@ export function TopCategoriesCard({ const categoryColor = getCategoryColorClasses( item.category.color, ); - const chartColor = - CHART_COLORS[index % CHART_COLORS.length]; + const chartColor = categoryBarColor( + item.category.color, + index, + ); return (
diff --git a/resources/js/hooks/use-chart-color-scheme.tsx b/resources/js/hooks/use-chart-color-scheme.tsx new file mode 100644 index 00000000..8d232f73 --- /dev/null +++ b/resources/js/hooks/use-chart-color-scheme.tsx @@ -0,0 +1,112 @@ +import { ChartColorScheme, SharedData } from '@/types'; +import { CategoryColor, getCategoryChartColor } from '@/types/category'; +import { usePage } from '@inertiajs/react'; +import { useCallback, useEffect, useState } from 'react'; + +const STORAGE_KEY = 'chart-color-scheme'; + +const setCookie = (name: string, value: string, days = 365) => { + if (typeof document === 'undefined') { + return; + } + + const maxAge = days * 24 * 60 * 60; + document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`; +}; + +const applyColorScheme = (scheme: ChartColorScheme) => { + if (typeof document === 'undefined') { + return; + } + + if (scheme === 'neutral') { + document.documentElement.removeAttribute('data-chart-color'); + } else { + document.documentElement.setAttribute('data-chart-color', scheme); + } +}; + +export function initializeChartColorScheme() { + if (typeof window === 'undefined') { + return; + } + + const saved = + (localStorage.getItem(STORAGE_KEY) as ChartColorScheme) || 'colorful'; + applyColorScheme(saved); +} + +export function useChartColorScheme() { + const { chartColorScheme: serverScheme } = usePage().props; + const [scheme, setScheme] = useState('colorful'); + + const updateScheme = useCallback((newScheme: ChartColorScheme) => { + setScheme(newScheme); + localStorage.setItem(STORAGE_KEY, newScheme); + setCookie(STORAGE_KEY, newScheme); + applyColorScheme(newScheme); + }, []); + + useEffect(() => { + const saved = localStorage.getItem( + STORAGE_KEY, + ) as ChartColorScheme | null; + updateScheme(saved || serverScheme || 'colorful'); + }, [serverScheme, updateScheme]); + + return { scheme, updateScheme } as const; +} + +/** + * Returns color helpers derived from the active chart color scheme. + * + * - `accountMainLineColor` – stroke for the main balance line in account cards + * - `accountGainLineColor` – stroke for the invested/gain line in account cards + * - `categoryBarColor` – progress bar color for a given category (colorful + * uses the category's own color; other schemes cycle + * through --chart-* variables by index) + */ +export function useChartColors() { + const { chartColorScheme } = usePage().props; + const isColorful = chartColorScheme === 'colorful'; + + const accountMainLineColor = isColorful + ? 'var(--account-line)' + : 'var(--color-chart-2)'; + + const accountGainLineColor = isColorful + ? 'var(--color-emerald-500)' + : 'var(--color-chart-6)'; + + const cashflowIncomeColor = isColorful + ? 'var(--cashflow-income)' + : 'var(--color-chart-2)'; + + const cashflowExpenseColor = isColorful + ? 'var(--cashflow-expense)' + : 'var(--color-chart-5)'; + + const CHART_COLORS = [ + 'var(--chart-1)', + 'var(--chart-2)', + 'var(--chart-3)', + 'var(--chart-4)', + 'var(--chart-5)', + 'var(--chart-6)', + 'var(--chart-7)', + 'var(--chart-8)', + ]; + + const categoryBarColor = (color: CategoryColor, index: number): string => + isColorful + ? getCategoryChartColor(color) + : CHART_COLORS[index % CHART_COLORS.length]; + + return { + accountMainLineColor, + accountGainLineColor, + cashflowIncomeColor, + cashflowExpenseColor, + categoryBarColor, + }; +} diff --git a/resources/js/pages/settings/appearance.tsx b/resources/js/pages/settings/appearance.tsx index db1dae46..754ff9c8 100644 --- a/resources/js/pages/settings/appearance.tsx +++ b/resources/js/pages/settings/appearance.tsx @@ -1,13 +1,20 @@ import { __ } from '@/utils/i18n'; -import { Head } from '@inertiajs/react'; +import { Head, router } from '@inertiajs/react'; import AppearanceTabs from '@/components/appearance-tabs'; import HeadingSmall from '@/components/heading-small'; -import { type BreadcrumbItem } from '@/types'; - +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useChartColorScheme } from '@/hooks/use-chart-color-scheme'; import AppLayout from '@/layouts/app-layout'; import SettingsLayout from '@/layouts/settings/layout'; import { edit as editAppearance } from '@/routes/appearance'; +import { type BreadcrumbItem, type ChartColorScheme } from '@/types'; const breadcrumbs: BreadcrumbItem[] = [ { @@ -16,7 +23,27 @@ const breadcrumbs: BreadcrumbItem[] = [ }, ]; +const schemes: { value: ChartColorScheme; label: string }[] = [ + { value: 'colorful', label: 'Colorful' }, + { value: 'neutral', label: 'Neutral' }, + { value: 'blue', label: 'Blue' }, + { value: 'pink', label: 'Pink' }, +]; + export default function Appearance() { + const { scheme, updateScheme } = useChartColorScheme(); + + const handleSchemeChange = (value: string) => { + const newScheme = value as ChartColorScheme; + updateScheme(newScheme); + + router.patch( + '/settings/chart-color-scheme', + { chart_color_scheme: newScheme }, + { preserveScroll: true }, + ); + }; + return ( @@ -32,6 +59,25 @@ export default function Appearance() {
+ +
+ + +
); diff --git a/resources/js/types/category.ts b/resources/js/types/category.ts index 88799001..1ab00227 100644 --- a/resources/js/types/category.ts +++ b/resources/js/types/category.ts @@ -204,3 +204,30 @@ export function getCategoryColorClasses(color: CategoryColor): { return colorMap[color]; } + +export function getCategoryChartColor(color: CategoryColor): string { + const colorMap: Record = { + amber: 'var(--color-amber-500)', + blue: 'var(--color-blue-500)', + cyan: 'var(--color-cyan-500)', + emerald: 'var(--color-emerald-500)', + fuchsia: 'var(--color-fuchsia-500)', + gray: 'var(--color-gray-500)', + green: 'var(--color-green-500)', + indigo: 'var(--color-indigo-500)', + lime: 'var(--color-lime-500)', + neutral: 'var(--color-neutral-500)', + orange: 'var(--color-orange-500)', + pink: 'var(--color-pink-500)', + purple: 'var(--color-purple-500)', + red: 'var(--color-red-500)', + rose: 'var(--color-rose-500)', + slate: 'var(--color-slate-500)', + stone: 'var(--color-stone-500)', + teal: 'var(--color-teal-500)', + violet: 'var(--color-violet-500)', + yellow: 'var(--color-yellow-500)', + }; + + return colorMap[color] ?? 'var(--color-gray-500)'; +} diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 6fe36e92..c2cde304 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -49,6 +49,8 @@ export interface Flash { error: string | null; } +export type ChartColorScheme = 'neutral' | 'colorful' | 'blue' | 'pink'; + export interface SharedData { name: string; appUrl: string; @@ -56,6 +58,7 @@ export interface SharedData { quote: { message: string; author: string }; auth: Auth; flash: Flash; + chartColorScheme: ChartColorScheme; subscriptionsEnabled: boolean; pricing: PricingConfig; sidebarOpen: boolean; diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index c4dc1ba8..e07d3b57 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -18,6 +18,11 @@ document.documentElement.classList.add('dark'); } } + + var chartScheme = localStorage.getItem('chart-color-scheme') || '{{ $chartColorScheme ?? "colorful" }}'; + if (chartScheme && chartScheme !== 'neutral') { + document.documentElement.setAttribute('data-chart-color', chartScheme); + } })(); diff --git a/routes/settings.php b/routes/settings.php index 2cace206..8913717d 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -4,6 +4,7 @@ use App\Http\Controllers\OpenBanking\ConnectionController; use App\Http\Controllers\Settings\AccountController; use App\Http\Controllers\Settings\BankController; use App\Http\Controllers\Settings\CategoryController; +use App\Http\Controllers\Settings\ChartColorSchemeController; use App\Http\Controllers\Settings\LabelController; use App\Http\Controllers\Settings\PasswordController; use App\Http\Controllers\Settings\ProfileController; @@ -55,6 +56,9 @@ Route::middleware('auth')->group(function () { return Inertia::render('settings/appearance'); })->name('appearance.edit'); + Route::patch('settings/chart-color-scheme', [ChartColorSchemeController::class, 'update']) + ->name('chart-color-scheme.update'); + Route::get('settings/billing', [SubscriptionController::class, 'billing'])->name('settings.billing'); Route::get('settings/billing/portal', [SubscriptionController::class, 'billingPortal'])->name('settings.billing.portal'); diff --git a/tests/Feature/Settings/ChartColorSchemeTest.php b/tests/Feature/Settings/ChartColorSchemeTest.php new file mode 100644 index 00000000..2a6396a8 --- /dev/null +++ b/tests/Feature/Settings/ChartColorSchemeTest.php @@ -0,0 +1,82 @@ +create(); + + $response = $this + ->actingAs($user) + ->patch(route('chart-color-scheme.update'), [ + 'chart_color_scheme' => 'blue', + ]); + + $response->assertSessionHasNoErrors()->assertRedirect(); + + expect($user->fresh()->setting->chart_color_scheme)->toBe(ChartColorScheme::Blue); +}); + +test('chart color scheme rejects invalid values', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch(route('chart-color-scheme.update'), [ + 'chart_color_scheme' => 'rainbow', + ]); + + $response->assertSessionHasErrors('chart_color_scheme'); +}); + +test('chart color scheme requires authentication', function () { + $response = $this->patch(route('chart-color-scheme.update'), [ + 'chart_color_scheme' => 'blue', + ]); + + $response->assertRedirect(route('login')); +}); + +test('chart color scheme creates setting when none exists', function () { + $user = User::factory()->create(); + + expect(UserSetting::where('user_id', $user->id)->exists())->toBeFalse(); + + $this->actingAs($user) + ->patch(route('chart-color-scheme.update'), [ + 'chart_color_scheme' => 'pink', + ]); + + expect(UserSetting::where('user_id', $user->id)->exists())->toBeTrue(); + expect($user->fresh()->setting->chart_color_scheme)->toBe(ChartColorScheme::Pink); +}); + +test('chart color scheme updates existing setting', function () { + $user = User::factory()->create(); + UserSetting::factory()->for($user)->withScheme(ChartColorScheme::Blue)->create(); + + $this->actingAs($user) + ->patch(route('chart-color-scheme.update'), [ + 'chart_color_scheme' => 'neutral', + ]); + + expect($user->fresh()->setting->chart_color_scheme)->toBe(ChartColorScheme::Neutral); +}); + +test('chart color scheme defaults to colorful when no setting exists', function () { + $user = User::factory()->create(); + + expect($user->setting)->toBeNull(); + expect($user->setting?->chart_color_scheme?->value ?? 'colorful')->toBe('colorful'); +}); + +test('chart color scheme is shared via inertia', function () { + $user = User::factory()->create(); + UserSetting::factory()->for($user)->withScheme(ChartColorScheme::Pink)->create(); + + $response = $this->actingAs($user)->get(route('appearance.edit')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page->where('chartColorScheme', 'pink')); +});