- {s.kind} · {s.jurisdiction} · {s.strategy} · leave UK y{s.leave_uk_year} ·{' '}
- {s.glide_path} glide · {s.horizon_years}y horizon
-
- {s.description &&
diff --git a/frontend/src/pages/ScenarioNew.tsx b/frontend/src/pages/ScenarioNew.tsx
new file mode 100644
index 0000000..3333895
--- /dev/null
+++ b/frontend/src/pages/ScenarioNew.tsx
@@ -0,0 +1,235 @@
+/**
+ * Create a user scenario via POST /scenarios. Redirects to the detail
+ * page on success.
+ */
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+import { api, type ScenarioCreateBody } from '@/api/client';
+
+const JURISDICTIONS = ['uk', 'cyprus', 'bulgaria', 'malaysia', 'thailand', 'uae', 'nomad'];
+const STRATEGIES = ['trinity', 'guyton_klinger', 'vpw', 'vpw_floor'];
+const GLIDES = ['rising', 'static_60_40'];
+
+const DEFAULTS: ScenarioCreateBody = {
+ name: '',
+ description: '',
+ jurisdiction: 'cyprus',
+ strategy: 'guyton_klinger',
+ leave_uk_year: 2,
+ glide_path: 'rising',
+ spending_gbp: '60000',
+ nw_seed_gbp: '1500000',
+ savings_per_year_gbp: '0',
+ horizon_years: 60,
+};
+
+export function ScenarioNew() {
+ const [form, setForm] = useState
(DEFAULTS);
+ const [nameError, setNameError] = useState(null);
+ const navigate = useNavigate();
+ const qc = useQueryClient();
+
+ const create = useMutation({
+ mutationFn: (body: ScenarioCreateBody) => api.scenarios.create(body),
+ onSuccess: (s) => {
+ qc.invalidateQueries({ queryKey: ['scenarios'] });
+ navigate(`/scenarios/${s.id}`);
+ },
+ });
+
+ const update = (key: K, value: ScenarioCreateBody[K]) =>
+ setForm((f) => ({ ...f, [key]: value }));
+
+ const onSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!form.name.trim()) {
+ setNameError('Required');
+ return;
+ }
+ setNameError(null);
+ create.mutate(form);
+ };
+
+ return (
+
+
+
+ ← Scenarios
+
+
+
+
+
+
+
+ );
+}
+
+function Field({
+ label,
+ error,
+ children,
+}: {
+ label: string;
+ error?: string | null;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+function Select({
+ value,
+ onChange,
+ options,
+}: {
+ value: string;
+ onChange: (v: string) => void;
+ options: string[];
+}) {
+ return (
+
+ );
+}
+
+function NumberInput({
+ value,
+ onChange,
+ min,
+ max,
+ step = 1,
+}: {
+ value: number;
+ onChange: (v: number) => void;
+ min?: number;
+ max?: number;
+ step?: number;
+}) {
+ return (
+ {
+ const n = Number(e.target.value);
+ if (Number.isFinite(n)) onChange(n);
+ }}
+ className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-slate-400"
+ />
+ );
+}
diff --git a/frontend/src/pages/Scenarios.tsx b/frontend/src/pages/Scenarios.tsx
index dbbd3c7..d634393 100644
--- a/frontend/src/pages/Scenarios.tsx
+++ b/frontend/src/pages/Scenarios.tsx
@@ -30,7 +30,15 @@ export function Scenarios() {
Persisted plans. Cartesian rebuilt from /recompute; user-defined survive.
-