2026-04-02 19:24:51 -04:00
|
|
|
|
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 '<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}</div>`;
|
|
|
|
|
|
return `<div class="fs-5 fw-semibold text-success">+$${mp-pp}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 `
|
2026-04-03 22:50:54 -04:00
|
|
|
|
<article class="border rounded-4 p-2 bg-body-tertiary inventory-entry-card"
|
|
|
|
|
|
data-inventory-id="${inv.inventoryId}"
|
|
|
|
|
|
data-card-id="${inv.cardId}"
|
|
|
|
|
|
data-purchase-price="${inv.purchasePrice}"
|
|
|
|
|
|
data-note="${(inv.note || '').replace(/"/g, '"')}">
|
2026-04-02 19:24:51 -04:00
|
|
|
|
<div class="d-flex flex-column gap-2">
|
|
|
|
|
|
<!-- Top row -->
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start gap-3">
|
|
|
|
|
|
<div class="min-w-0 flex-grow-1">
|
|
|
|
|
|
<div class="fw-semibold fs-5 text-body mb-1">${inv.condition}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Middle row -->
|
|
|
|
|
|
<div class="row g-2">
|
|
|
|
|
|
<div class="col-4">
|
|
|
|
|
|
<div class="small text-secondary">Purchase price</div>
|
|
|
|
|
|
<div class="fs-5 fw-semibold">$${inv.purchasePrice}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-4">
|
|
|
|
|
|
<div class="small text-secondary">Market price</div>
|
|
|
|
|
|
<div class="fs-5 text-success">$${inv.sku?.marketPrice}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-4">
|
|
|
|
|
|
<div class="small text-secondary">Gain / loss</div>
|
|
|
|
|
|
${GainLoss(inv.purchasePrice, inv.sku?.marketPrice)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Bottom row -->
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
|
|
|
|
|
<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">
|
2026-04-03 22:50:54 -04:00
|
|
|
|
<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>
|
2026-04-02 19:24:51 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
2026-04-03 22:50:54 -04:00
|
|
|
|
<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">Remove</button>
|
2026-04-02 19:24:51 -04:00
|
|
|
|
</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) => {
|
|
|
|
|
|
// 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:
|
2026-04-03 22:10:41 -04:00
|
|
|
|
// No action = list inventory for this card
|
|
|
|
|
|
return getInventory(userId!, cardId);
|
2026-04-02 19:24:51 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 22:10:41 -04:00
|
|
|
|
// Always return current inventory after a mutation
|
2026-04-02 19:24:51 -04:00
|
|
|
|
return getInventory(userId!, cardId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
};
|