From 3be17fe84c91d4574f3f7eba1c94971864874cf4 Mon Sep 17 00:00:00 2001 From: Thad Miller Date: Thu, 2 Apr 2026 19:24:51 -0400 Subject: [PATCH] [wip] bugs to work out, but backend should support inventory --- scripts/pokemon-helper.ts | 30 ++++++ scripts/reindex.ts | 7 +- src/db/relations.ts | 12 +++ src/db/schema.ts | 17 ++- src/pages/api/inventory.ts | 161 ++++++++++++++++++++++++++++ src/pages/dashboard.astro | 2 +- src/pages/partials/card-modal.astro | 106 ++---------------- src/pages/partials/cards.astro | 5 +- 8 files changed, 240 insertions(+), 100 deletions(-) create mode 100644 src/pages/api/inventory.ts diff --git a/scripts/pokemon-helper.ts b/scripts/pokemon-helper.ts index a3c05b8..9a9a9c9 100644 --- a/scripts/pokemon-helper.ts +++ b/scripts/pokemon-helper.ts @@ -88,6 +88,25 @@ export const createSkuCollection = async () => { console.log(chalk.green('Collection "skus" created successfully.')); } +// Delete and recreate the 'inventory' index +export const createInventoryCollection = async () => { + try { + await client.collections('inventories').delete(); + } catch (error) { + // Ignore error, just means collection doesn't exist + } + await client.collections().create({ + name: 'inventories', + fields: [ + { name: 'id', type: 'string' }, + { name: 'userId', type: 'string' }, + { name: 'catalogName', type: 'string' }, + { name: 'card_id', type: 'string', reference: 'cards.id' }, + ] + }); + console.log(chalk.green('Collection "inventories" created successfully.')); +} + export const upsertCardCollection = async (db:DBInstance) => { const pokemon = await db.query.cards.findMany({ @@ -131,6 +150,17 @@ export const upsertSkuCollection = async (db:DBInstance) => { console.log(chalk.green('Collection "skus" indexed successfully.')); } +export const upsertInventoryCollection = async (db:DBInstance) => { + const inv = await db.query.inventory.findMany(); + await client.collections('inventories').documents().import(inv.map(i => ({ + id: i.inventoryId, + userId: i.userId, + catalogName: i.catalogName, + card_id: i.cardId.toString(), + })), { action: 'upsert' }); + console.log(chalk.green('Collection "inventories" indexed successfully.')); +} + diff --git a/scripts/reindex.ts b/scripts/reindex.ts index 7a93288..71e4eed 100644 --- a/scripts/reindex.ts +++ b/scripts/reindex.ts @@ -5,7 +5,10 @@ import * as Indexing from './pokemon-helper.ts'; //await Indexing.createCardCollection(); //await Indexing.createSkuCollection(); -await Indexing.upsertCardCollection(db); -await Indexing.upsertSkuCollection(db); +await Indexing.createInventoryCollection(); + +//await Indexing.upsertCardCollection(db); +//await Indexing.upsertSkuCollection(db); +await Indexing.upsertInventoryCollection(db); await ClosePool(); console.log(chalk.green('Pokémon reindex complete.')); diff --git a/src/db/relations.ts b/src/db/relations.ts index 218d423..0ecc14d 100644 --- a/src/db/relations.ts +++ b/src/db/relations.ts @@ -21,9 +21,21 @@ export const relations = defineRelations(schema, (r) => ({ }), history: r.many.priceHistory(), latestSales: r.many.salesHistory(), + inventories: r.many.inventory(), + }, + inventory: { + card: r.one.cards({ + from: r.inventory.cardId, + to: r.cards.cardId, + }), + sku: r.one.skus({ + from: [r.inventory.cardId, r.inventory.condition], + to: [r.skus.cardId, r.skus.condition], + }), }, cards: { prices: r.many.skus(), + inventories: r.many.inventory(), set: r.one.sets({ from: r.cards.setId, to: r.sets.setId, diff --git a/src/db/schema.ts b/src/db/schema.ts index 5c16cc7..f61fdcc 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,5 +1,5 @@ //import { mysqlTable, int, varchar, boolean, decimal, datetime, index } from "drizzle-orm/mysql-core" -import { integer, varchar, boolean, decimal, timestamp, index, pgSchema, uniqueIndex, primaryKey } from "drizzle-orm/pg-core"; +import { integer, varchar, boolean, decimal, timestamp, index, pgSchema, uuid, primaryKey } from "drizzle-orm/pg-core"; export const pokeSchema = pgSchema("pokemon"); @@ -98,6 +98,7 @@ export const skus = pokeSchema.table('skus', { }, (table) => [ index('idx_product_id_condition').on(table.productId, table.variant, table.condition), + index('idx_card_id_condition').on(table.cardId, table.condition), ]); export const priceHistory = pokeSchema.table('price_history', { @@ -124,6 +125,20 @@ export const salesHistory = pokeSchema.table('sales_history',{ primaryKey({ name: 'pk_sales_history', columns: [table.skuId, table.orderDate] }) ]); +export const inventory = pokeSchema.table('inventory',{ + inventoryId: uuid().primaryKey().notNull().defaultRandom(), + userId: varchar({ length: 100 }).notNull(), + catalogName: varchar({ length: 100 }), + cardId: integer().notNull(), + condition: varchar({ length: 255 }).notNull(), + quantity: integer(), + purchasePrice: integer(), + note: varchar({ length:255 }) +}, +(table) => [ + index('idx_userid_cardid').on(table.userId, table.cardId) +]); + export const processingSkus = pokeSchema.table('processing_skus', { skuId: integer().primaryKey(), }); diff --git a/src/pages/api/inventory.ts b/src/pages/api/inventory.ts new file mode 100644 index 0000000..1f661e6 --- /dev/null +++ b/src/pages/api/inventory.ts @@ -0,0 +1,161 @@ +import type { APIRoute } from 'astro'; +import { db } from '../../db/index'; +import { inventory } from '../../db/schema'; +import { client } from '../../db/typesense'; +import { eq } from 'drizzle-orm'; + + +const GainLoss = (purchasePrice:any, marketPrice:any) => { + if (!purchasePrice || !marketPrice) return '
N/A
'; + const pp = Number(purchasePrice); + const mp = Number(marketPrice); + if (pp === mp) return '
-
'; + if (pp > mp) return `
-$${pp-mp}
`; + return `
+$${mp-pp}
`; +} + + +const getInventory = async (userId:string, cardId:number) => { + + const inventories = await db.query.inventory.findMany({ + where: { userId:userId, cardId:cardId, }, + with: { card: true, sku: true, } + }); + + const invHtml = inventories.map(inv => { + return ` +
+
+ +
+
+
${inv.condition}
+
+
+ +
+
+
Purchase price
+
$${inv.purchasePrice}
+
+
+
Market price
+
$${inv.sku?.marketPrice}
+
+
+
Gain / loss
+ ${GainLoss(inv.purchasePrice, inv.sku?.marketPrice)} +
+
+ +
+
+ Qty +
+ + + +
+
+
+ + +
+
+
+
`; + }); + + return new Response( + invHtml.join(''), + { + status: 200, + headers: { 'Content-Type': 'text/html' }, + } + ); + +} + + +const addToInventory = async (userId:string, cardId:number, condition:string, purchasePrice:number, quantity:number, note:string, catalogName:string) => { + // First add to database + const inv = await db.insert(inventory).values({ + userId: userId, + cardId: cardId, + catalogName: catalogName, + condition: condition, + purchasePrice: purchasePrice, + quantity: quantity, + note: note, + }).returning(); + // And then add to Typesense + await client.collections('inventories').documents().import(inv.map(i => ({ + id: i.inventoryId, + userId: i.userId, + catalogName: i.catalogName, + card_id: i.cardId.toString(), + }))); +} + +const removeFromInventory = async (inventoryId:string) => { + await db.delete(inventory).where(eq( inventory.inventoryId, inventoryId )); + await client.collections('inventories').documents(inventoryId).delete(); +} + +const updateInventory = async (inventoryId:string, quantity:number, purchasePrice:number, note:string) => { + // Update in database + await db.update(inventory).set({ + quantity: quantity, + purchasePrice: purchasePrice, + note: note, + }).where(eq( inventory.inventoryId, inventoryId )); + // There is no need to update Typesense for these fields as they are not indexed +} + +export const POST: APIRoute = async ({ request, locals }) => { + // Access form data from the request body + const formData = await request.formData(); + const action = formData.get('action'); + const cardId = Number(formData.get('cardId')) || 0; + const { userId } = locals.auth(); + + switch (action) { + + case 'add': + const condition = formData.get('condition')?.toString() || 'Unknown'; + const purchasePrice = Number(formData.get('purchasePrice')) || 0; + const quantity = Number(formData.get('quantity')) || 1; + const note = formData.get('note')?.toString() || ''; + const catalogName = formData.get('catalogName')?.toString() || 'Default'; + await addToInventory(userId!, cardId, condition, purchasePrice, quantity, note, catalogName); + //return await getInventory(cardId); + break; + + case 'remove': + const inventoryId = formData.get('inventoryId')?.toString() || ''; + await removeFromInventory(inventoryId); + break; + + case 'update': + const invId = formData.get('inventoryId')?.toString() || ''; + const qty = Number(formData.get('quantity')) || 1; + const price = Number(formData.get('purchasePrice')) || 0; + const invNote = formData.get('note')?.toString() || ''; + await updateInventory(invId, qty, price, invNote); + break; + + default: + return new Response( + 'Invalid action', + { + status: 400, + headers: { 'Content-Type': 'text/html' } + } + ); + } + + // Always return current inventory + return getInventory(userId!, cardId); + + +}; \ No newline at end of file diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index ab941f3..1ebb508 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -1028,7 +1028,7 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0); document.getElementById("catalogList")?.appendChild(li); input.value = ""; - bootstrap.Modal.getInstance(document.getElementById("newCatalogModal"))?.hide(); + //bootstrap.Modal.getInstance(document.getElementById("newCatalogModal"))?.hide(); }); document.getElementById("csvFileInput")?.addEventListener("change", (e) => { diff --git a/src/pages/partials/card-modal.astro b/src/pages/partials/card-modal.astro index f19f510..7c9ee58 100644 --- a/src/pages/partials/card-modal.astro +++ b/src/pages/partials/card-modal.astro @@ -169,7 +169,6 @@ const altSearchUrl = (card: any) => { return `https://alt.xyz/browse?query=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&sortBy=newest_first`; }; --- -