import { Head, router } from '@inertiajs/react'; import { useState } from 'react'; import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import InlineEditInput from '@/components/InlineEditInput'; import InlineEditSelect from '@/components/InlineEditSelect'; import IncomeDistributionPreview from '@/components/IncomeDistributionPreview'; interface Scenario { id: string; name: string; distribution_mode: 'even' | 'priority'; distribution_mode_label: string; created_at: string; updated_at: string; } 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 Stream { id: string; name: string; type: 'income' | 'expense'; type_label: string; amount: number; frequency: string; frequency_label: string; start_date: string; end_date: string | null; bucket_id: string | null; bucket_name: string | null; description: string | null; is_active: boolean; monthly_equivalent: number; } interface StreamStats { total_streams: number; active_streams: number; income_streams: number; expense_streams: number; monthly_income: number; monthly_expenses: number; monthly_net: number; } interface Props { scenario: Scenario; buckets: { data: Bucket[] }; streams: { data: Stream[] }; streamStats?: StreamStats; } const bucketTypeBorderColor = { need: 'border-blue-500', want: 'border-green-500', overflow: 'border-amber-500', } as const; const bucketTypeOptions = [ { value: 'need', label: 'Need' }, { value: 'want', label: 'Want' }, ]; /** Convert cents to dollars for display */ const centsToDollars = (cents: number): number => cents / 100; /** Convert dollars to cents for storage */ const dollarsToCents = (dollars: number): number => Math.round(dollars * 100); /** Convert basis points to percent for display */ const basisPointsToPercent = (bp: number): number => bp / 100; /** Convert percent to basis points for storage */ 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 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', 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, streams = { data: [] }, streamStats }: Props) { const [isModalOpen, setIsModalOpen] = 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(`Are you sure you want to 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'] }); } else { console.error('Failed to delete bucket'); } } 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 */}

{scenario.name}

Water flows through the pipeline into prioritized buckets

{/* Bucket Dashboard */}

Buckets

{buckets.data.length === 0 ? ( Set up your buckets Income flows through your pipeline into prioritized buckets. Start by creating buckets for your spending categories.

Need

Essential expenses: rent, bills, groceries

Want

Discretionary spending: dining, hobbies, entertainment

Overflow

Catches all remaining funds — one per scenario

) : (
{buckets.data.map((bucket) => (

{bucket.name}

Priority {bucket.priority} •{' '} patchBucket(bucket.id, { type: val })} displayLabel={bucket.type_label} disabled={bucket.type === 'overflow'} /> {' '}• {bucket.allocation_type_label}

#{bucket.priority}
Current Filling patchBucket(bucket.id, { starting_amount: dollarsToCents(val) })} formatDisplay={(v) => `$${v.toFixed(2)}`} min={0} step="0.01" className="text-lg font-semibold text-gray-900" />
Allocation: {formatAllocationValue(bucket)} {bucket.allocation_type === 'fixed_limit' && ( ( patchBucket(bucket.id, { buffer_multiplier: val })} formatDisplay={(v) => v > 0 ? `${v}x buffer` : 'no buffer'} min={0} step="0.01" /> {bucket.buffer_multiplier > 0 && bucket.effective_capacity !== null && ( <> = ${centsToDollars(bucket.effective_capacity).toFixed(2)} effective )} ) )}
{bucket.allocation_type === 'fixed_limit' && (
Progress ${centsToDollars(bucket.current_balance).toFixed(2)} / ${bucket.effective_capacity !== null ? centsToDollars(bucket.effective_capacity).toFixed(2) : '∞'}
)}
{bucket.type !== 'overflow' && (
)}
))}
)} {/* Income Distribution Preview */} {buckets.data.length > 0 && (
)} {/* Streams Section */}

Income & Expense Streams

{/* Stream Statistics */} {streamStats && (
Monthly Income
${streamStats.monthly_income.toFixed(2)}
Monthly Expenses
${streamStats.monthly_expenses.toFixed(2)}
Net Cash Flow
= 0 ? 'text-green-600' : 'text-red-600'}`}> ${streamStats.monthly_net.toFixed(2)}
Active Streams
{streamStats.active_streams} / {streamStats.total_streams}
)} {streams.data.length === 0 ? (

No streams yet. Add income or expense streams to start tracking cash flow.

) : (
{streams.data.map((stream) => ( ))}
Name Type Amount Frequency Bucket Start Date Status Actions
{stream.name} {stream.type_label} ${stream.amount.toFixed(2)} {stream.frequency_label} {stream.bucket_name || '-'} {new Date(stream.start_date).toLocaleDateString()}
)}
{/* Create Bucket Modal */} {isModalOpen && (

Add New Bucket

setFormData({ ...formData, name: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-gray-900" placeholder="e.g., Travel Fund" required />
{formData.allocation_type !== 'unlimited' && (
setFormData({ ...formData, allocation_value: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-gray-900" 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 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-gray-900" placeholder="e.g., 0.75" step="0.01" min="0" /> )}
)}
)} ); }