diff --git a/resources/js/components/AppSidebarHeader.tsx b/resources/js/components/AppSidebarHeader.tsx index 29a0c45..cfcc4d5 100644 --- a/resources/js/components/AppSidebarHeader.tsx +++ b/resources/js/components/AppSidebarHeader.tsx @@ -1,18 +1,41 @@ import { Breadcrumbs } from '@/components/Breadcrumbs'; +import SettingsPanel from '@/components/SettingsPanel'; +import { Button } from '@/components/ui/button'; import { SidebarTrigger } from '@/components/ui/sidebar'; import { type BreadcrumbItem as BreadcrumbItemType } from '@/types'; +import { Settings } from 'lucide-react'; +import { useState } from 'react'; export function AppSidebarHeader({ breadcrumbs = [], }: { breadcrumbs?: BreadcrumbItemType[]; }) { + const [settingsOpen, setSettingsOpen] = useState(false); + return ( + + + setSettingsOpen(true)} + > + + Settings + + + + ); } diff --git a/resources/js/components/SettingsPanel.tsx b/resources/js/components/SettingsPanel.tsx new file mode 100644 index 0000000..9534dc2 --- /dev/null +++ b/resources/js/components/SettingsPanel.tsx @@ -0,0 +1,142 @@ +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { csrfToken } from '@/lib/utils'; +import { type SharedData } from '@/types'; +import { router, usePage } from '@inertiajs/react'; +import { useState } from 'react'; + +type SaveStatus = 'idle' | 'saving' | 'success' | 'error'; +type DistributionMode = 'even' | 'priority'; + +interface DistributionOption { + value: DistributionMode; + label: string; + description: string; +} + +const distributionOptions: DistributionOption[] = [ + { + value: 'even', + label: 'Even Split', + description: + 'Split evenly across buckets in each phase, respecting individual capacity', + }, + { + value: 'priority', + label: 'Priority Order', + description: 'Fill highest-priority bucket first, then next', + }, +]; + +interface SettingsPanelProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function SettingsPanel({ + open, + onOpenChange, +}: SettingsPanelProps) { + const { scenario } = usePage().props; + const [saveStatus, setSaveStatus] = useState('idle'); + + if (!scenario) return null; + + const handleDistributionModeChange = async (value: DistributionMode) => { + if (value === scenario.distribution_mode) return; + + setSaveStatus('saving'); + try { + const response = await fetch(`/scenarios/${scenario.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken(), + }, + body: JSON.stringify({ distribution_mode: value }), + }); + + if (!response.ok) throw new Error('Failed to update'); + + setSaveStatus('success'); + router.reload({ only: ['scenario', 'buckets'] }); + setTimeout(() => setSaveStatus('idle'), 1500); + } catch { + setSaveStatus('error'); + setTimeout(() => setSaveStatus('idle'), 1500); + } + }; + + return ( + + + + Settings + + + + + + Distribution Mode + {saveStatus === 'success' && ( + + Saved + + )} + {saveStatus === 'error' && ( + + Failed to save + + )} + + + How income is divided across buckets within each + phase + + + {distributionOptions.map((option) => ( + + + handleDistributionModeChange( + e.target.value as DistributionMode, + ) + } + className="mt-0.5" + /> + + + {option.label} + + + {option.description} + + + + ))} + + + + + + ); +} diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts index 61fbf3a..9ebe773 100644 --- a/resources/js/lib/utils.ts +++ b/resources/js/lib/utils.ts @@ -16,3 +16,11 @@ export function isSameUrl( export function resolveUrl(url: NonNullable): string { return typeof url === 'string' ? url : url.url; } + +export function csrfToken(): string { + return ( + document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute('content') || '' + ); +} diff --git a/resources/js/pages/Scenarios/Show.tsx b/resources/js/pages/Scenarios/Show.tsx index b65bbcf..725c59b 100644 --- a/resources/js/pages/Scenarios/Show.tsx +++ b/resources/js/pages/Scenarios/Show.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import InlineEditInput from '@/components/InlineEditInput'; import InlineEditSelect from '@/components/InlineEditSelect'; import IncomeDistributionPreview from '@/components/IncomeDistributionPreview'; +import { csrfToken } from '@/lib/utils'; interface Scenario { id: string; @@ -97,9 +98,6 @@ const formatAllocationValue = (bucket: Bucket): string => { return `$${centsToDollars(bucket.allocation_value).toFixed(2)}`; }; -const csrfToken = () => - document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; - const patchBucket = async (bucketId: string, data: Record): Promise => { const response = await fetch(`/buckets/${bucketId}`, { method: 'PATCH',
+ How income is divided across buckets within each + phase +