From 8926ad637485882706c0e2211d9d0ee6e1359a3c Mon Sep 17 00:00:00 2001
From: myrmidex
Date: Mon, 29 Dec 2025 21:53:52 +0100
Subject: [PATCH] Scenarios crud
---
app/Http/Controllers/ScenarioController.php | 41 +++++
app/Models/Scenario.php | 49 ++++++
composer.json | 2 +-
composer.lock | 2 +-
database/factories/ScenarioFactory.php | 19 +++
...25_12_29_192203_create_scenarios_table.php | 22 +++
docker-compose.yml | 15 --
resources/js/pages/Scenarios/Index.tsx | 151 ++++++++++++++++++
resources/js/pages/Scenarios/Show.tsx | 68 ++++++++
routes/web.php | 10 +-
tests/Feature/Auth/AuthenticationTest.php | 8 +
tests/Feature/Auth/PasswordResetTest.php | 7 +-
tests/Feature/Auth/RegistrationTest.php | 2 +
tests/Feature/Auth/TwoFactorChallengeTest.php | 2 +
.../Auth/VerificationNotificationTest.php | 4 +
tests/Feature/Settings/PasswordUpdateTest.php | 4 +
tests/Feature/Settings/ProfileUpdateTest.php | 8 +
17 files changed, 391 insertions(+), 23 deletions(-)
create mode 100644 app/Http/Controllers/ScenarioController.php
create mode 100644 app/Models/Scenario.php
create mode 100644 database/factories/ScenarioFactory.php
create mode 100644 database/migrations/2025_12_29_192203_create_scenarios_table.php
create mode 100644 resources/js/pages/Scenarios/Index.tsx
create mode 100644 resources/js/pages/Scenarios/Show.tsx
diff --git a/app/Http/Controllers/ScenarioController.php b/app/Http/Controllers/ScenarioController.php
new file mode 100644
index 0000000..5808802
--- /dev/null
+++ b/app/Http/Controllers/ScenarioController.php
@@ -0,0 +1,41 @@
+ Scenario::orderBy('created_at', 'desc')->get()
+ ]);
+ }
+
+ public function show(Scenario $scenario): Response
+ {
+ return Inertia::render('Scenarios/Show', [
+ 'scenario' => $scenario
+ ]);
+ }
+
+ public function store(Request $request): RedirectResponse
+ {
+ $request->validate([
+ 'name' => 'required|string|max:255',
+ ]);
+
+ $scenario = Scenario::create([
+ 'name' => $request->name,
+ ]);
+
+ // TODO: Create default buckets here (will implement later with bucket model)
+
+ return redirect()->route('scenarios.show', $scenario);
+ }
+}
diff --git a/app/Models/Scenario.php b/app/Models/Scenario.php
new file mode 100644
index 0000000..354b091
--- /dev/null
+++ b/app/Models/Scenario.php
@@ -0,0 +1,49 @@
+ */
+ use HasFactory;
+
+ protected $fillable = [
+ 'name',
+ ];
+
+
+
+ public function buckets(): HasMany
+ {
+ return $this->hasMany(Bucket::class);
+ }
+
+ /**
+ * Get the streams for this scenario.
+ */
+ public function streams(): HasMany
+ {
+ return $this->hasMany(Stream::class);
+ }
+
+ /**
+ * Get the inflows for this scenario.
+ */
+ public function inflows(): HasMany
+ {
+ return $this->hasMany(Inflow::class);
+ }
+
+ /**
+ * Get the outflows for this scenario.
+ */
+ public function outflows(): HasMany
+ {
+ return $this->hasMany(Outflow::class);
+ }
+}
diff --git a/composer.json b/composer.json
index 50a10f4..5ef4352 100644
--- a/composer.json
+++ b/composer.json
@@ -94,4 +94,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index 2517057..9123215 100644
--- a/composer.lock
+++ b/composer.lock
@@ -8847,5 +8847,5 @@
"php": "^8.2"
},
"platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}
diff --git a/database/factories/ScenarioFactory.php b/database/factories/ScenarioFactory.php
new file mode 100644
index 0000000..f95f816
--- /dev/null
+++ b/database/factories/ScenarioFactory.php
@@ -0,0 +1,19 @@
+
+ */
+class ScenarioFactory extends Factory
+{
+ public function definition(): array
+ {
+ return [
+ 'name' => $this->faker->words(2, true) . ' Budget',
+ ];
+ }
+}
diff --git a/database/migrations/2025_12_29_192203_create_scenarios_table.php b/database/migrations/2025_12_29_192203_create_scenarios_table.php
new file mode 100644
index 0000000..3226989
--- /dev/null
+++ b/database/migrations/2025_12_29_192203_create_scenarios_table.php
@@ -0,0 +1,22 @@
+id();
+ $table->string('name');
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('scenarios');
+ }
+};
diff --git a/docker-compose.yml b/docker-compose.yml
index 5020ef8..8e22f26 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -85,21 +85,6 @@ services:
networks:
- buckets
- # Selenium for E2E testing with Dusk
- selenium:
- image: selenium/standalone-chrome:latest
- container_name: buckets_selenium
- restart: unless-stopped
- ports:
- - "4445:4444" # Selenium server
- - "7901:7900" # VNC server for debugging
- volumes:
- - /dev/shm:/dev/shm
- networks:
- - buckets
- environment:
- - SE_VNC_PASSWORD=secret
-
# Optional: Redis for caching/sessions
# redis:
# image: redis:alpine
diff --git a/resources/js/pages/Scenarios/Index.tsx b/resources/js/pages/Scenarios/Index.tsx
new file mode 100644
index 0000000..c917956
--- /dev/null
+++ b/resources/js/pages/Scenarios/Index.tsx
@@ -0,0 +1,151 @@
+import { Head, router } from '@inertiajs/react';
+import React, { useState } from 'react';
+
+interface Scenario {
+ id: number;
+ name: string;
+ created_at: string;
+ updated_at: string;
+}
+
+interface Props {
+ scenarios: Scenario[];
+}
+
+export default function Index({ scenarios }: Props) {
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [name, setName] = useState('');
+ const [errors, setErrors] = useState<{ name?: string }>({});
+
+ const handleCreateScenario = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ router.post('/scenarios', { name }, {
+ onSuccess: () => {
+ setShowCreateForm(false);
+ setName('');
+ setErrors({});
+ },
+ onError: (errors) => {
+ setErrors(errors);
+ }
+ });
+ };
+
+ const handleCancel = () => {
+ setShowCreateForm(false);
+ setName('');
+ setErrors({});
+ };
+
+ return (
+ <>
+
+
+
+
+
+
Budget Scenarios
+
+ Manage your budget projections with water-themed scenarios
+
+
+
+ {/* Create Scenario Form */}
+ {showCreateForm && (
+
+
Create New Scenario
+
+
+ )}
+
+ {/* Create Button */}
+ {!showCreateForm && (
+
+
+
+ )}
+
+ {/* Scenarios List */}
+ {scenarios.length === 0 ? (
+
+
+
No scenarios yet
+
+ Create your first budget scenario to get started
+
+
+ ) : (
+
+ {scenarios.map((scenario) => (
+
router.visit(`/scenarios/${scenario.id}`)}
+ className="cursor-pointer rounded-lg bg-white p-6 shadow transition-shadow hover:shadow-lg"
+ >
+
+ {scenario.name}
+
+
+ Created {new Date(scenario.created_at).toLocaleDateString()}
+
+
+
+ ))}
+
+ )}
+
+
+ >
+ );
+}
diff --git a/resources/js/pages/Scenarios/Show.tsx b/resources/js/pages/Scenarios/Show.tsx
new file mode 100644
index 0000000..f0ce8b4
--- /dev/null
+++ b/resources/js/pages/Scenarios/Show.tsx
@@ -0,0 +1,68 @@
+import { Head, Link } from '@inertiajs/react';
+
+interface Scenario {
+ id: number;
+ name: string;
+ created_at: string;
+ updated_at: string;
+}
+
+interface Props {
+ scenario: Scenario;
+}
+
+export default function Show({ scenario }: Props) {
+ return (
+ <>
+