import { Head, Link, 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'; interface Scenario { id: string; name: 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; current_balance: number; has_available_space: boolean; available_space: number; } 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 BUFFER_PRESETS = ['0', '0.5', '1', '1.5', '2'] as const; 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 `${Number(bucket.allocation_value).toFixed(2)}%`; return `$${Number(bucket.allocation_value).toFixed(2)}`; }; 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 [editingBucket, setEditingBucket] = useState(null); const [formData, setFormData] = useState({ ...defaultFormData }); const openCreateModal = () => { setEditingBucket(null); setFormData({ ...defaultFormData }); setIsModalOpen(true); }; const handleEdit = (bucket: Bucket) => { setEditingBucket(bucket); const bufferStr = bucket.buffer_multiplier.toString(); const isPreset = (BUFFER_PRESETS as readonly string[]).includes(bufferStr); setFormData({ name: bucket.name, type: bucket.type, allocation_type: bucket.allocation_type, allocation_value: bucket.allocation_value ? bucket.allocation_value.toString() : '', buffer_multiplier: bufferStr, buffer_mode: isPreset ? 'preset' : 'custom', }); 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': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', }, }); 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); const url = editingBucket ? `/buckets/${editingBucket.id}` : `/scenarios/${scenario.id}/buckets`; const method = editingBucket ? 'PATCH' : 'POST'; try { const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', }, body: JSON.stringify({ name: formData.name, type: formData.type, allocation_type: formData.allocation_type, allocation_value: formData.allocation_value ? parseFloat(formData.allocation_value) : null, buffer_multiplier: formData.allocation_type === 'fixed_limit' ? parseFloat(formData.buffer_multiplier) || 0 : 0, priority: editingBucket ? editingBucket.priority : undefined }), }); if (response.ok) { setIsModalOpen(false); setFormData({ ...defaultFormData }); setEditingBucket(null); router.reload({ only: ['buckets'] }); } else { const errorData = await response.json(); console.error(`Failed to ${editingBucket ? 'update' : 'create'} bucket:`, errorData); } } catch (error) { console.error(`Error ${editingBucket ? 'updating' : '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; // Don't allow moving beyond bounds if (newPriority < 1 || newPriority > buckets.data.length) return; try { const response = await fetch(`/buckets/${bucketId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', }, body: JSON.stringify({ name: bucket.name, type: bucket.type, allocation_type: bucket.allocation_type, allocation_value: bucket.allocation_value, buffer_multiplier: bucket.buffer_multiplier, priority: newPriority }), }); if (response.ok) { router.reload({ only: ['buckets'] }); } else { console.error('Failed to update bucket priority'); } } catch (error) { console.error('Error updating bucket priority:', error); } }; return ( <>
{/* Header */}
Back to Scenarios

{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} • {bucket.type_label} • {bucket.allocation_type_label}

#{bucket.priority}
Current Balance ${bucket.current_balance.toFixed(2)}
Allocation: {formatAllocationValue(bucket)} {bucket.allocation_type === 'fixed_limit' && bucket.buffer_multiplier > 0 && ( ({bucket.buffer_multiplier}x buffer = ${bucket.effective_capacity.toFixed(2)} effective) )}
{bucket.allocation_type === 'fixed_limit' && (
Progress ${bucket.current_balance.toFixed(2)} / ${bucket.effective_capacity.toFixed(2)}
)}
{bucket.type !== 'overflow' && ( )}
))}
)} {/* 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()}
)}
{/* Placeholder for future features */}

Coming Next: Timeline & Projections

Calculate projections to see your money flow through these buckets over time.

{/* Add Bucket Modal */} {isModalOpen && (

{editingBucket ? 'Edit Bucket' : '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 />
{/* Hide type select for overflow buckets (type is fixed) */} {!(editingBucket && editingBucket.type === 'overflow') && (
)} {/* Hide allocation type select for overflow (always unlimited) */} {formData.type !== 'overflow' && (
)} {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" /> )}
)}
)} ); }