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
This commit is contained in:
2026-05-14 10:29:27 +02:00
parent 3340fd11ca
commit 043c3d5afe
4 changed files with 62 additions and 23 deletions
+29 -7
View File
@@ -221,7 +221,25 @@ export default function Analyze() {
updatePositions(); updatePositions();
}, [stocks.length]); }, [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)); setStocks((s) => s.filter((stock) => stock.id !== id));
}; };
@@ -346,12 +364,16 @@ export default function Analyze() {
> >
{stock.loading ? "Running..." : "Analyze"} {stock.loading ? "Running..." : "Analyze"}
</button> </button>
<button <button
onClick={() => removeStock(stock.id)} onClick={async () => {
className="bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700" if (confirm(`Remove ${stock.ticker}?`)) {
> await removeStock(stock.id);
Delete }
</button> }}
className="bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700"
>
Delete
</button>
</div> </div>
</td> </td>
</tr> </tr>
+8
View File
@@ -10,11 +10,19 @@ export async function loader() {
export async function action({ request }: { request: Request }) { export async function action({ request }: { request: Request }) {
const formData = await request.formData(); const formData = await request.formData();
const ticker = formData.get("ticker")?.toString().toUpperCase(); const ticker = formData.get("ticker")?.toString().toUpperCase();
const method = formData.get("_method")?.toString() || "POST";
if (!ticker) { if (!ticker) {
return Response.json({ error: "Ticker is required" }, { status: 400 }); 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({ const stock = await db.stock.create({
data: { ticker }, data: { ticker },
}); });
BIN
View File
Binary file not shown.
+22 -13
View File
@@ -1,31 +1,40 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
test.describe("Stock Database", () => { test.describe("Stock Database", () => {
test("should add and list stocks", async ({ request }) => { test("should add and list stocks", async ({ page }) => {
const uniqueTicker = `TEST${Date.now()}`; const uniqueTicker = `TEST${Date.now()}`;
const createRes = await request.post("/api/stocks", { const createRes = await page.request.post("/api/stocks", {
form: { ticker: uniqueTicker }, data: new URLSearchParams({ ticker: uniqueTicker }).toString(),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
}); });
expect(createRes.ok()).toBeTruthy(); expect(createRes.ok()).toBeTruthy();
const listRes = await request.get("/api/stocks"); const listRes = await page.request.get("/api/stocks");
const stocks = await listRes.json(); const stocks = await listRes.json();
expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker })); expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker }));
}); });
test("should persist tickers after page reload", async ({ request, page }) => { test("should delete stock from database", async ({ page }) => {
const uniqueTicker = `PERSIST${Date.now()}`; const uniqueTicker = `DEL${Date.now()}`;
await request.post("/api/stocks", { await page.request.post("/api/stocks", {
form: { ticker: uniqueTicker }, data: new URLSearchParams({ ticker: uniqueTicker }).toString(),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
}); });
await page.goto("/stocks"); let listRes = await page.request.get("/api/stocks");
await page.waitForLoadState("networkidle"); let stocks = await listRes.json();
const listRes = await request.get("/api/stocks");
const stocks = await listRes.json();
expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker })); 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 }));
}); });
}); });