import { Head, router } from '@inertiajs/react'; import { Settings } from 'lucide-react'; import { useState } from 'react'; import InlineEditInput from '@/components/InlineEditInput'; import InlineEditSelect from '@/components/InlineEditSelect'; import SettingsPanel from '@/components/SettingsPanel'; import { csrfToken } from '@/lib/utils'; import { type Scenario } from '@/types'; interface Bucket { id: string; name: string; type: 'need' | 'want' | 'overflow'; type_label: string; priority: number; sort_order: number; allocation_type: string; allocation_value: number | null; allocation_type_label: string; buffer_multiplier: number; effective_capacity: number | null; starting_amount: number; current_balance: number; has_available_space: boolean; available_space: number | null; } interface Props { scenario: Scenario; buckets: { data: Bucket[] }; } const bucketTypeOptions = [ { value: 'need', label: 'Need' }, { value: 'want', label: 'Want' }, ]; const allocationTypeOptions = [ { value: 'fixed_limit', label: 'Fixed Limit' }, { value: 'percentage', label: 'Percentage' }, ]; const centsToDollars = (cents: number): number => cents / 100; const dollarsToCents = (dollars: number): number => Math.round(dollars * 100); const basisPointsToPercent = (bp: number): number => bp / 100; const percentToBasisPoints = (pct: number): number => Math.round(pct * 100); const formatAllocationValue = (bucket: Bucket): string => { if (bucket.allocation_type === 'unlimited') return 'ALL REMAINING'; if (bucket.allocation_value === null) return '--'; if (bucket.allocation_type === 'percentage') return `${basisPointsToPercent(bucket.allocation_value).toFixed(2)}%`; return `$${centsToDollars(bucket.allocation_value).toFixed(2)}`; }; const patchBucket = async (bucketId: string, data: Record): Promise => { const response = await fetch(`/buckets/${bucketId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error('Failed to update bucket'); } router.reload({ only: ['buckets'] }); }; const defaultFormData = { name: '', type: 'need' as Bucket['type'], allocation_type: 'fixed_limit', allocation_value: '', buffer_multiplier: '0', buffer_mode: 'preset' as 'preset' | 'custom', }; export default function Show({ scenario, buckets }: Props) { const [isModalOpen, setIsModalOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ ...defaultFormData }); const openCreateModal = () => { setFormData({ ...defaultFormData }); setIsModalOpen(true); }; const handleDelete = async (bucket: Bucket) => { if (!confirm(`Delete "${bucket.name}"?`)) return; try { const response = await fetch(`/buckets/${bucket.id}`, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': csrfToken() }, }); if (response.ok) { router.reload({ only: ['buckets'] }); } } catch (error) { console.error('Error deleting bucket:', error); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); try { const response = await fetch(`/scenarios/${scenario.id}/buckets`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken(), }, body: JSON.stringify({ name: formData.name, type: formData.type, allocation_type: formData.allocation_type, allocation_value: formData.allocation_value ? (formData.allocation_type === 'percentage' ? percentToBasisPoints(parseFloat(formData.allocation_value)) : dollarsToCents(parseFloat(formData.allocation_value))) : null, buffer_multiplier: formData.allocation_type === 'fixed_limit' ? parseFloat(formData.buffer_multiplier) || 0 : 0, }), }); if (response.ok) { setIsModalOpen(false); setFormData({ ...defaultFormData }); router.reload({ only: ['buckets'] }); } else { const errorData = await response.json(); console.error('Failed to create bucket:', errorData); } } catch (error) { console.error('Error creating bucket:', error); } finally { setIsSubmitting(false); } }; const handlePriorityChange = async (bucketId: string, direction: 'up' | 'down') => { const bucket = buckets.data.find(b => b.id === bucketId); if (!bucket) return; const newPriority = direction === 'up' ? bucket.priority - 1 : bucket.priority + 1; if (newPriority < 1 || newPriority > buckets.data.length) return; try { await patchBucket(bucketId, { priority: newPriority }); } catch (error) { console.error('Error updating bucket priority:', error); } }; return ( <>
{/* Header */}

BUCKETS

{/* Buckets */}

BUCKETS

{buckets.data.length === 0 ? (

NO BUCKETS CONFIGURED

Income flows through your pipeline into prioritized buckets.

) : (
{buckets.data.map((bucket) => (
{/* Header: name + priority */}

patchBucket(bucket.id, { name: val })} />

patchBucket(bucket.id, { type: val })} displayLabel={bucket.type_label} disabled={bucket.type === 'overflow'} /> {' | '} patchBucket(bucket.id, { allocation_type: val, allocation_value: null })} displayLabel={bucket.allocation_type_label} disabled={bucket.type === 'overflow'} />

#{bucket.priority}
{/* Filling */}
Current patchBucket(bucket.id, { starting_amount: dollarsToCents(val) })} formatDisplay={(v) => `$${v.toFixed(2)}`} min={0} step="0.01" className="font-digital text-red-500" />
{/* Allocation */}
Allocation {bucket.allocation_type === 'fixed_limit' ? ( patchBucket(bucket.id, { allocation_value: dollarsToCents(val) })} formatDisplay={(v) => `$${v.toFixed(2)}`} min={0} step="0.01" /> ) : bucket.allocation_type === 'percentage' ? ( patchBucket(bucket.id, { allocation_value: percentToBasisPoints(val) })} formatDisplay={(v) => `${v.toFixed(2)}%`} min={0} step="0.01" /> ) : ( formatAllocationValue(bucket) )}
{/* Buffer */} {bucket.allocation_type === 'fixed_limit' && (
Buffer patchBucket(bucket.id, { buffer_multiplier: val })} formatDisplay={(v) => v > 0 ? `${v}x` : 'NONE'} min={0} step="0.01" />
)} {/* Progress bar placeholder — will be replaced with DigitalProgressBar */} {bucket.allocation_type === 'fixed_limit' && bucket.effective_capacity !== null && (
PROGRESS ${centsToDollars(bucket.current_balance).toFixed(2)} / ${centsToDollars(bucket.effective_capacity).toFixed(2)}
)} {/* Delete */} {bucket.type !== 'overflow' && (
)}
))}
)}
{/* Create Bucket Modal */} {isModalOpen && (

ADD BUCKET

setFormData({ ...formData, name: e.target.value })} className="w-full bg-black border-2 border-red-500/50 px-3 py-2 text-red-500 font-mono focus:border-red-500 focus:outline-none" placeholder="e.g., Travel Fund" required />
{formData.allocation_type !== 'unlimited' && (
setFormData({ ...formData, allocation_value: e.target.value })} className="w-full bg-black border-2 border-red-500/50 px-3 py-2 text-red-500 font-mono font-digital focus:border-red-500 focus:outline-none" placeholder={formData.allocation_type === 'percentage' ? '25' : '1000'} step={formData.allocation_type === 'percentage' ? '0.01' : '0.01'} min={formData.allocation_type === 'percentage' ? '0.01' : '0'} max={formData.allocation_type === 'percentage' ? '100' : undefined} required />
)} {formData.allocation_type === 'fixed_limit' && (
{formData.buffer_mode === 'custom' && ( setFormData({ ...formData, buffer_multiplier: e.target.value })} className="mt-2 w-full bg-black border-2 border-red-500/50 px-3 py-2 text-red-500 font-mono font-digital focus:border-red-500 focus:outline-none" placeholder="e.g., 0.75" step="0.01" min="0" /> )}
)}
)} ); }