196 lines
8.4 KiB
TypeScript
196 lines
8.4 KiB
TypeScript
import type { APIRoute } from 'astro';
|
||
import { db } from '../../db/index';
|
||
import { inventory, priceHistory } 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-6 fw-semibold">N/A</div>';
|
||
const pp = Number(purchasePrice);
|
||
const mp = Number(marketPrice);
|
||
if (pp === mp) return '<div class="fs-6 fw-semibold text-warning">-</div>';
|
||
if (pp > mp) return `<div class="fs-6 fw-semibold text-danger">-$${(pp - mp).toFixed(2)}</div>`;
|
||
return `<div class="fs-6 fw-semibold text-success">+$${(mp - pp).toFixed(2)}</div>`;
|
||
}
|
||
const DollarToInt = (dollar: any) => {
|
||
if (dollar === null) return null;
|
||
return Math.round(dollar * 100);
|
||
}
|
||
|
||
const getInventory = async (userId: string, cardId: number) => {
|
||
|
||
const card = await db.query.cards.findFirst({
|
||
where: { cardId: cardId, },
|
||
with : { prices: {
|
||
with: { inventories: { where: { userId: userId } }, }
|
||
}, },
|
||
});
|
||
|
||
const invHtml = card?.prices?.flatMap(price => price.inventories.map(inv => {
|
||
const marketPrice = price.marketPrice;
|
||
const marketPriceDisplay = marketPrice ? `$${marketPrice}` : '—';
|
||
const purchasePriceDisplay = inv.purchasePrice ? `$${Number(inv.purchasePrice).toFixed(2)}` : '—';
|
||
|
||
return `
|
||
<article class="border rounded-4 p-2 inventory-entry-card"
|
||
data-inventory-id="${inv.inventoryId}"
|
||
data-card-id="${price.cardId}"
|
||
data-purchase-price="${inv.purchasePrice}"
|
||
data-note="${(inv.note || '').replace(/"/g, '"')}">
|
||
<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">${price.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, skuId: number, purchasePrice: number, quantity: number, note: string, catalogName: string) => {
|
||
// First add to database
|
||
const inv = await db.insert(inventory).values({
|
||
userId: userId,
|
||
skuId: skuId,
|
||
catalogName: catalogName,
|
||
purchasePrice: purchasePrice.toFixed(2),
|
||
quantity: quantity,
|
||
note: note,
|
||
}).returning();
|
||
// Get card details from the database to add to Typesense
|
||
const card = await db.query.cards.findFirst({
|
||
where: { cardId: cardId },
|
||
with: { set: true },
|
||
});
|
||
|
||
try {
|
||
// And then add to Typesense for searching
|
||
await client.collections('inventories').documents().import(inv.map(i => ({
|
||
id: i.inventoryId,
|
||
userId: i.userId,
|
||
catalogName: i.catalogName,
|
||
sku_id: i.skuId.toString(),
|
||
purchasePrice: DollarToInt(i.purchasePrice),
|
||
productLineName: card?.productLineName,
|
||
rarityName: card?.rarityName,
|
||
setName: card?.set?.setName || "",
|
||
cardType: card?.cardType || "",
|
||
energyType: card?.energyType || "",
|
||
card_id: card?.cardId.toString() || "",
|
||
content: [
|
||
card?.productName,
|
||
card?.productLineName,
|
||
card?.set?.setName || "",
|
||
card?.number,
|
||
card?.rarityName,
|
||
card?.artist || ""
|
||
].join(' '),
|
||
})));
|
||
} catch (error) {
|
||
console.error('Error adding inventory to Typesense:', error);
|
||
}
|
||
}
|
||
|
||
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 the database
|
||
await db.update(inventory).set({
|
||
quantity: quantity,
|
||
purchasePrice: purchasePrice.toFixed(2),
|
||
note: note,
|
||
}).where(eq(inventory.inventoryId, inventoryId));
|
||
// No need to update Typesense since we don't search by quantity or price
|
||
}
|
||
|
||
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 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';
|
||
const condition = formData.get('condition')?.toString() || 'Near Mint';
|
||
const skuId = await db.query.skus.findFirst({
|
||
where: { cardId: cardId, condition: condition },
|
||
columns: { skuId: true },
|
||
}).then(sku => sku?.skuId);
|
||
if (!skuId) {
|
||
return new Response('SKU not found for card', { status: 404 });
|
||
}
|
||
await addToInventory(userId!, cardId, skuId, 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:
|
||
// No action = list inventory for this card
|
||
return getInventory(userId!, cardId);
|
||
}
|
||
|
||
// Always return current inventory after a mutation
|
||
return getInventory(userId!, cardId);
|
||
}; |