From a045ee6c2375fd8e4204d6129197355b43a73bcf Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 21 Mar 2026 10:47:36 +0100 Subject: [PATCH] 13 - Integrate inline editing into bucket cards --- resources/js/pages/Scenarios/Show.tsx | 236 ++++++++++++-------------- 1 file changed, 112 insertions(+), 124 deletions(-) diff --git a/resources/js/pages/Scenarios/Show.tsx b/resources/js/pages/Scenarios/Show.tsx index 7ec88fd..29c490f 100644 --- a/resources/js/pages/Scenarios/Show.tsx +++ b/resources/js/pages/Scenarios/Show.tsx @@ -2,6 +2,8 @@ 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'; interface Scenario { id: string; @@ -22,6 +24,7 @@ interface Bucket { allocation_type_label: string; buffer_multiplier: number; effective_capacity: number; + starting_amount: number; current_balance: number; has_available_space: boolean; available_space: number; @@ -67,7 +70,10 @@ const bucketTypeBorderColor = { overflow: 'border-amber-500', } as const; -const BUFFER_PRESETS = ['0', '0.5', '1', '1.5', '2'] as const; +const bucketTypeOptions = [ + { value: 'need', label: 'Need' }, + { value: 'want', label: 'Want' }, +]; const formatAllocationValue = (bucket: Bucket): string => { if (bucket.allocation_type === 'unlimited') return 'All remaining'; @@ -76,6 +82,26 @@ const formatAllocationValue = (bucket: Bucket): string => { return `$${Number(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'], @@ -88,30 +114,13 @@ const defaultFormData = { 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; @@ -120,9 +129,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream try { const response = await fetch(`/buckets/${bucket.id}`, { method: 'DELETE', - headers: { - 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', - }, + headers: { 'X-CSRF-TOKEN': csrfToken() }, }); if (response.ok) { @@ -139,18 +146,12 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream 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, + const response = await fetch(`/scenarios/${scenario.id}/buckets`, { + method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', + 'X-CSRF-TOKEN': csrfToken(), }, body: JSON.stringify({ name: formData.name, @@ -158,21 +159,19 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream 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); + console.error('Failed to create bucket:', errorData); } } catch (error) { - console.error(`Error ${editingBucket ? 'updating' : 'creating'} bucket:`, error); + console.error('Error creating bucket:', error); } finally { setIsSubmitting(false); } @@ -183,32 +182,10 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream 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'); - } + await patchBucket(bucketId, { priority: newPriority }); } catch (error) { console.error('Error updating bucket priority:', error); } @@ -217,7 +194,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream return ( <> - +
{/* Header */} @@ -241,7 +218,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream + Add Bucket
- + {buckets.data.length === 0 ? ( @@ -286,7 +263,15 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream {bucket.name}

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

@@ -318,19 +303,35 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
- Current Balance - - ${bucket.current_balance.toFixed(2)} - + Current Filling + patchBucket(bucket.id, { starting_amount: val })} + formatDisplay={(v) => `$${v.toFixed(2)}`} + min={0} + step="1" + className="text-lg font-semibold text-gray-900" + />
Allocation: {formatAllocationValue(bucket)} - {bucket.allocation_type === 'fixed_limit' && bucket.buffer_multiplier > 0 && ( + {bucket.allocation_type === 'fixed_limit' && ( - ({bucket.buffer_multiplier}x buffer = ${bucket.effective_capacity.toFixed(2)} effective) + ( + 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.toFixed(2)} effective + )} + ) )}
@@ -355,38 +356,32 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream )}
-
- - {bucket.type !== 'overflow' && ( + {bucket.type !== 'overflow' && ( +
- )} -
+
+ )}
))} )} - + {/* Streams Section */}

Income & Expense Streams

-
- + {/* Stream Statistics */} {streamStats && (
@@ -432,7 +427,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
)} - + {streams.data.length === 0 ? (

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

@@ -476,8 +471,8 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream {stream.type_label} @@ -498,8 +493,8 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
)} - + {/* Placeholder for future features */}

@@ -535,12 +530,12 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream

- {/* Add Bucket Modal */} + {/* Create Bucket Modal */} {isModalOpen && (
-

{editingBucket ? 'Edit Bucket' : 'Add New Bucket'}

+

Add New Bucket

- {/* 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' && (
@@ -657,7 +646,6 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream type="button" onClick={() => { setIsModalOpen(false); - setEditingBucket(null); setFormData({ ...defaultFormData }); }} className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300" @@ -670,7 +658,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50" disabled={isSubmitting} > - {isSubmitting ? (editingBucket ? 'Updating...' : 'Creating...') : (editingBucket ? 'Update Bucket' : 'Create Bucket')} + {isSubmitting ? 'Creating...' : 'Create Bucket'}
@@ -680,4 +668,4 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream )} ); -} \ No newline at end of file +}