From 043c3d5afe6e3f3833dc9e0ffd709095ae0dc5c9 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Thu, 14 May 2026 10:29:27 +0200 Subject: [PATCH] feat: delete ticker from database when removed from portfolio - Add DELETE support to /api/stocks endpoint via _method parameter - Modify removeStock to delete db- prefixed entries from database - Add confirmation dialog on delete button click - Add test for stock deletion --- app/routes/analyze.tsx | 36 +++++++++++++++++++++++------ app/routes/api/stocks/index.ts | 8 +++++++ prisma/dev.db | Bin 24576 -> 24576 bytes tests/stock-db.spec.ts | 41 ++++++++++++++++++++------------- 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/app/routes/analyze.tsx b/app/routes/analyze.tsx index 941830e..8c8fa53 100644 --- a/app/routes/analyze.tsx +++ b/app/routes/analyze.tsx @@ -221,7 +221,25 @@ export default function Analyze() { updatePositions(); }, [stocks.length]); - const removeStock = (id: string) => { + const removeStock = async (id: string) => { + const stock = stocks.find((s) => s.id === id); + if (!stock) return; + + // Delete from database if this was a manually added stock (db- prefix) + if (id.startsWith("db-")) { + try { + const formData = new FormData(); + formData.append("_method", "DELETE"); + formData.append("ticker", stock.ticker); + await fetch("/api/stocks", { + method: "POST", + body: formData, + }); + } catch (err) { + console.error("[analyze] Error deleting stock from DB:", err); + } + } + setStocks((s) => s.filter((stock) => stock.id !== id)); }; @@ -346,12 +364,16 @@ export default function Analyze() { > {stock.loading ? "Running..." : "Analyze"} - + diff --git a/app/routes/api/stocks/index.ts b/app/routes/api/stocks/index.ts index a7765ee..f2d58e6 100644 --- a/app/routes/api/stocks/index.ts +++ b/app/routes/api/stocks/index.ts @@ -10,11 +10,19 @@ export async function loader() { export async function action({ request }: { request: Request }) { const formData = await request.formData(); const ticker = formData.get("ticker")?.toString().toUpperCase(); + const method = formData.get("_method")?.toString() || "POST"; if (!ticker) { return Response.json({ error: "Ticker is required" }, { status: 400 }); } + if (method === "DELETE") { + await db.stock.deleteMany({ + where: { ticker }, + }); + return Response.json({ success: true }); + } + const stock = await db.stock.create({ data: { ticker }, }); diff --git a/prisma/dev.db b/prisma/dev.db index a4c8a0e76078060029f226aca1411dccd121eab2..292d18674931e06d0866db13541f310a3e81ec01 100644 GIT binary patch delta 437 zcmZoTz}Rqrae_3Xr^Jxw(b8iMg?fxq*=h<2=<_6Jj8wjS!o? zK0xe2)S9p<_$Bm5bx8;><)@-e9XL0d5>=v6qv~yCChBj zh|@p9NV1p?7J|!SbC2L=MZd?4%tCy*li34+b~f|n^5p{U)Z%j#Vb*0t4=Q0%W_?B^ VKNy>Uf { - test("should add and list stocks", async ({ request }) => { + test("should add and list stocks", async ({ page }) => { const uniqueTicker = `TEST${Date.now()}`; - - const createRes = await request.post("/api/stocks", { - form: { ticker: uniqueTicker }, + + const createRes = await page.request.post("/api/stocks", { + data: new URLSearchParams({ ticker: uniqueTicker }).toString(), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, }); expect(createRes.ok()).toBeTruthy(); - - const listRes = await request.get("/api/stocks"); + + const listRes = await page.request.get("/api/stocks"); const stocks = await listRes.json(); expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker })); }); - test("should persist tickers after page reload", async ({ request, page }) => { - const uniqueTicker = `PERSIST${Date.now()}`; - - await request.post("/api/stocks", { - form: { ticker: uniqueTicker }, + test("should delete stock from database", async ({ page }) => { + const uniqueTicker = `DEL${Date.now()}`; + + await page.request.post("/api/stocks", { + data: new URLSearchParams({ ticker: uniqueTicker }).toString(), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, }); - await page.goto("/stocks"); - await page.waitForLoadState("networkidle"); - - const listRes = await request.get("/api/stocks"); - const stocks = await listRes.json(); + let listRes = await page.request.get("/api/stocks"); + let stocks = await listRes.json(); expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker })); + + const delRes = await page.request.post("/api/stocks", { + data: new URLSearchParams({ ticker: uniqueTicker, _method: "DELETE" }).toString(), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }); + expect(delRes.ok()).toBeTruthy(); + + listRes = await page.request.get("/api/stocks"); + stocks = await listRes.json(); + expect(stocks).not.toContainEqual(expect.objectContaining({ ticker: uniqueTicker })); }); }); \ No newline at end of file