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:
+29
-7
@@ -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>
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
+22
-13
@@ -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 }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user