import { Head, Link, router } from '@inertiajs/react';
import { useState } from 'react';
interface Scenario {
id: number;
name: string;
created_at: string;
updated_at: string;
}
interface Bucket {
id: number;
name: string;
priority: number;
sort_order: number;
allocation_type: string;
allocation_value: number | null;
allocation_type_label: string;
formatted_allocation_value: string;
current_balance: number;
has_available_space: boolean;
available_space: number;
}
interface Stream {
id: number;
name: string;
type: 'income' | 'expense';
type_label: string;
amount: number;
frequency: string;
frequency_label: string;
start_date: string;
end_date: string | null;
bucket_id: number | 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;
}
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({
name: '',
allocation_type: 'fixed_limit',
allocation_value: ''
});
const handleEdit = (bucket: Bucket) => {
setEditingBucket(bucket);
setFormData({
name: bucket.name,
allocation_type: bucket.allocation_type,
allocation_value: bucket.allocation_value ? bucket.allocation_value.toString() : ''
});
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,
allocation_type: formData.allocation_type,
allocation_value: formData.allocation_value ? parseFloat(formData.allocation_value) : null,
priority: editingBucket ? editingBucket.priority : undefined
}),
});
if (response.ok) {
setIsModalOpen(false);
setFormData({ name: '', allocation_type: 'fixed_limit', allocation_value: '' });
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: number, 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,
allocation_type: bucket.allocation_type,
allocation_value: bucket.allocation_value,
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.map((bucket) => (
{bucket.name}
Priority {bucket.priority} • {bucket.allocation_type_label}
#{bucket.priority}
Current Balance
${bucket.current_balance.toFixed(2)}
Allocation: {bucket.formatted_allocation_value}
{bucket.allocation_type === 'fixed_limit' && (
Progress
${bucket.current_balance.toFixed(2)} / ${Number(bucket.allocation_value)?.toFixed(2)}
)}
))}
{/* Virtual Overflow Bucket (placeholder for now) */}
Overflow
Unallocated funds
$0.00
{/* 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.
) : (
|
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()}
|
|
|
))}
)}
{/* 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'}
)}
>
);
}