143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
|
|
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<SharedData>().props;
|
||
|
|
const [saveStatus, setSaveStatus] = useState<SaveStatus>('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 (
|
||
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||
|
|
<SheetContent side="top" className="px-6 pb-6">
|
||
|
|
<SheetHeader>
|
||
|
|
<SheetTitle>Settings</SheetTitle>
|
||
|
|
</SheetHeader>
|
||
|
|
|
||
|
|
<div className="mx-auto max-w-2xl">
|
||
|
|
<fieldset disabled={saveStatus === 'saving'}>
|
||
|
|
<legend className="text-sm font-medium text-foreground">
|
||
|
|
Distribution Mode
|
||
|
|
{saveStatus === 'success' && (
|
||
|
|
<span className="ml-2 text-green-600">
|
||
|
|
Saved
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
{saveStatus === 'error' && (
|
||
|
|
<span className="ml-2 text-red-600">
|
||
|
|
Failed to save
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</legend>
|
||
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
||
|
|
How income is divided across buckets within each
|
||
|
|
phase
|
||
|
|
</p>
|
||
|
|
<div className="mt-3 space-y-2">
|
||
|
|
{distributionOptions.map((option) => (
|
||
|
|
<label
|
||
|
|
key={option.value}
|
||
|
|
className={`flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors ${
|
||
|
|
scenario.distribution_mode ===
|
||
|
|
option.value
|
||
|
|
? 'border-primary bg-primary/5'
|
||
|
|
: 'border-border hover:border-primary/50'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="radio"
|
||
|
|
name="distribution_mode"
|
||
|
|
value={option.value}
|
||
|
|
checked={
|
||
|
|
scenario.distribution_mode ===
|
||
|
|
option.value
|
||
|
|
}
|
||
|
|
onChange={(e) =>
|
||
|
|
handleDistributionModeChange(
|
||
|
|
e.target.value as DistributionMode,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
className="mt-0.5"
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<div className="text-sm font-medium">
|
||
|
|
{option.label}
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-muted-foreground">
|
||
|
|
{option.description}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</fieldset>
|
||
|
|
</div>
|
||
|
|
</SheetContent>
|
||
|
|
</Sheet>
|
||
|
|
);
|
||
|
|
}
|