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';
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 */}
{scenario.name}
Water flows through the pipeline into prioritized buckets
{/* Bucket Dashboard */}
Buckets
+ Add Bucket
{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
Create Your First Bucket
) : (
{buckets.data.map((bucket) => (
{bucket.name}
Priority {bucket.priority} • {bucket.type_label} • {bucket.allocation_type_label}
handlePriorityChange(bucket.id, 'up')}
disabled={bucket.priority === 1}
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Move up in priority"
>
handlePriorityChange(bucket.id, 'down')}
disabled={bucket.priority === buckets.data.length}
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Move down in priority"
>
#{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)}
)}
handleEdit(bucket)}
className="flex-1 text-sm text-blue-600 hover:text-blue-500"
>
Edit
{bucket.type !== 'overflow' && (
handleDelete(bucket)}
className="flex-1 text-sm text-red-600 hover:text-red-500"
>
Delete
)}
))}
)}
{/* Streams Section */}
Income & Expense Streams
+ Add Stream
{/* 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.
) : (
Name
Type
Amount
Frequency
Bucket
Start Date
Status
Actions
{streams.data.map((stream) => (
{stream.name}
{stream.type_label}
${stream.amount.toFixed(2)}
{stream.frequency_label}
{stream.bucket_name || '-'}
{new Date(stream.start_date).toLocaleDateString()}
{stream.is_active ? 'Active' : 'Inactive'}
Edit
Delete
))}
)}
{/* 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'}
)}
>
);
}