Files
pokemon/src/pages/api/inventory.ts

175 lines
7.4 KiB
TypeScript
Raw Normal View History

import type { APIRoute } from 'astro';
import { db } from '../../db/index';
import { inventory, skus, cards } from '../../db/schema';
import { client } from '../../db/typesense';
import { eq, and } from 'drizzle-orm';
const GainLoss = (purchasePrice: any, marketPrice: any) => {
if (!purchasePrice || !marketPrice) return '<div class="fs-5 fw-semibold">N/A</div>';
const pp = Number(purchasePrice);
const mp = Number(marketPrice);
if (pp === mp) return '<div class="fs-5 fw-semibold text-warning">-</div>';
if (pp > mp) return `<div class="fs-5 fw-semibold text-critical">-$${(pp - mp).toFixed(2)}</div>`;
return `<div class="fs-6 fw-semibold text-success">+$${(mp - pp).toFixed(2)}</div>`;
}
const getInventory = async (userId: string, cardId: number) => {
const inventories = await db
.select({
inventoryId: inventory.inventoryId,
cardId: inventory.cardId,
condition: inventory.condition,
quantity: inventory.quantity,
purchasePrice: inventory.purchasePrice,
note: inventory.note,
marketPrice: skus.marketPrice,
createdAt: inventory.createdAt,
})
.from(inventory)
.leftJoin(
cards,
eq(inventory.cardId, cards.cardId)
)
.leftJoin(
skus,
and(
eq(cards.productId, skus.productId),
eq(inventory.condition, skus.condition)
)
)
.where(and(
eq(inventory.userId, userId),
eq(inventory.cardId, cardId)
));
const invHtml = inventories.map(inv => {
const marketPrice = inv.marketPrice ? Number(inv.marketPrice).toFixed(2) : null;
const marketPriceDisplay = marketPrice ? `$${marketPrice}` : '—';
const purchasePriceDisplay = inv.purchasePrice ? `$${Number(inv.purchasePrice).toFixed(2)}` : '—';
return `
<article class="alert alert-dark rounded-4 inventory-entry-card"
data-inventory-id="${inv.inventoryId}"
data-card-id="${inv.cardId}"
data-purchase-price="${inv.purchasePrice}"
data-note="${(inv.note || '').replace(/"/g, '&quot;')}">
<div class="d-flex flex-column">
<!-- Top row -->
<div class="d-flex justify-content-between gap-3">
<div class="min-w-0 flex-grow-1">
<div class="fw-semibold fs-6 text-body mb-1">${inv.condition}</div>
</div>
<div class="fs-7 text-secondary">Added: ${inv.createdAt ? new Date(inv.createdAt).toLocaleDateString() : '—'}</div>
</div>
<!-- Middle row -->
<div class="row g-2">
<div class="col-4">
<div class="small text-secondary">Purchase price</div>
<div class="fs-6 fw-semibold">${purchasePriceDisplay}</div>
</div>
<div class="col-4">
<div class="small text-secondary">Market price</div>
<div class="fs-6 text-success">${marketPriceDisplay}</div>
</div>
<div class="col-4">
<div class="small text-secondary">Gain / loss</div>
${GainLoss(inv.purchasePrice, marketPrice)}
</div>
</div>
<!-- Bottom row -->
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap mt-2">
<div class="d-flex align-items-center gap-2">
<span class="small text-secondary">Qty</span>
<div class="btn-group" role="group" aria-label="Quantity controls">
<button type="button" class="btn btn-outline-secondary btn-sm" data-inv-action="decrement"></button>
<button type="button" class="btn btn-outline-secondary btn-sm" tabindex="-1" data-inv-qty>${inv.quantity}</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-inv-action="increment">+</button>
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="btn btn-sm btn-outline-secondary" data-inv-action="update">Edit</button>
<button type="button" class="btn btn-sm btn-outline-danger" data-inv-action="remove" onclick="if(!confirm('Are you sure you want to remove this card from your inventory?')) event.stopImmediatePropagation();">Remove</button>
</div>
</div>
</div>
</article>`;
});
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) => {
const inv = await db.insert(inventory).values({
userId: userId,
cardId: cardId,
catalogName: catalogName,
condition: condition,
purchasePrice: purchasePrice,
quantity: quantity,
note: note,
}).returning();
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) => {
await db.update(inventory).set({
quantity: quantity,
purchasePrice: purchasePrice,
note: note,
}).where(eq(inventory.inventoryId, inventoryId));
}
export const POST: APIRoute = async ({ request, locals }) => {
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);
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:
2026-04-03 22:10:41 -04:00
return getInventory(userId!, cardId);
}
return getInventory(userId!, cardId);
};