1189 lines
38 KiB
Plaintext
1189 lines
38 KiB
Plaintext
|
|
---
|
|||
|
|
import Layout from "../layouts/Main.astro";
|
|||
|
|
import NavBar from "../components/NavBar.astro";
|
|||
|
|
import NavItems from "../components/NavItems.astro";
|
|||
|
|
import Footer from "../components/Footer.astro";
|
|||
|
|
import FirstEditionIcon from "../components/FirstEditionIcon.astro";
|
|||
|
|
|
|||
|
|
// Mock inventory using the same schema as the Typesense cards collection.
|
|||
|
|
// skus mirror the real shape: marketPrice is in cents (÷100 = dollars).
|
|||
|
|
const inventory = [
|
|||
|
|
{
|
|||
|
|
productId: "3830",
|
|||
|
|
productName: "Charizard",
|
|||
|
|
setName: "Base Set",
|
|||
|
|
setCode: "BS",
|
|||
|
|
number: "4/102",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Fire",
|
|||
|
|
variant: "1st Edition",
|
|||
|
|
qty: 2,
|
|||
|
|
purchasePrice: 32000,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 40000 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 31000 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 22000 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 14000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 8500 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "3959",
|
|||
|
|
productName: "Pikachu",
|
|||
|
|
setName: "Shining Legends",
|
|||
|
|
setCode: "SLG",
|
|||
|
|
number: "SM70",
|
|||
|
|
rarityName: "Promo",
|
|||
|
|
energyType: "Lightning",
|
|||
|
|
variant: "Normal",
|
|||
|
|
qty: 5,
|
|||
|
|
purchasePrice: 1500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 2000 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 1500 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 1100 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 700 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 400 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "3729",
|
|||
|
|
productName: "Mewtwo",
|
|||
|
|
setName: "Fossil",
|
|||
|
|
setCode: "FO",
|
|||
|
|
number: "10/62",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Psychic",
|
|||
|
|
variant: "Normal",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 12000,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 13000 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 10000 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 7200 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 4500 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 2500 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4114",
|
|||
|
|
productName: "Umbreon VMAX",
|
|||
|
|
setName: "Evolving Skies",
|
|||
|
|
setCode: "EVS",
|
|||
|
|
number: "215/203",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Darkness",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 8500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 11500 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 9000 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 6500 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 4000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 2000 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "3887",
|
|||
|
|
productName: "Gyarados",
|
|||
|
|
setName: "Hidden Fates",
|
|||
|
|
setCode: "HIF",
|
|||
|
|
number: "SV19/SV94",
|
|||
|
|
rarityName: "Shiny Holo Rare",
|
|||
|
|
energyType: "Water",
|
|||
|
|
variant: "Shiny",
|
|||
|
|
qty: 3,
|
|||
|
|
purchasePrice: 2500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 3000 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 2300 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 1600 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 900 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 500 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4201",
|
|||
|
|
productName: "Rayquaza VMAX",
|
|||
|
|
setName: "Evolving Skies",
|
|||
|
|
setCode: "EVS",
|
|||
|
|
number: "218/203",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Dragon",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 2,
|
|||
|
|
purchasePrice: 6500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 8800 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 7000 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 5000 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 3200 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 1800 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4055",
|
|||
|
|
productName: "Eevee",
|
|||
|
|
setName: "Sword & Shield",
|
|||
|
|
setCode: "SSH",
|
|||
|
|
number: "TG07/TG30",
|
|||
|
|
rarityName: "Trainer Gallery",
|
|||
|
|
energyType: "Colorless",
|
|||
|
|
variant: "Normal",
|
|||
|
|
qty: 10,
|
|||
|
|
purchasePrice: 800,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 900 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 700 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 500 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 300 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 150 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "3991",
|
|||
|
|
productName: "Lugia V",
|
|||
|
|
setName: "Silver Tempest",
|
|||
|
|
setCode: "SIT",
|
|||
|
|
number: "186/195",
|
|||
|
|
rarityName: "Ultra Rare",
|
|||
|
|
energyType: "Colorless",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 4500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 5800 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 4600 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 3200 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 2000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 1000 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4301",
|
|||
|
|
productName: "Blastoise",
|
|||
|
|
setName: "Base Set",
|
|||
|
|
setCode: "BS",
|
|||
|
|
number: "2/102",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Water",
|
|||
|
|
variant: "Shadowless",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 18000,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 24000 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 18500 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 13000 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 8000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 4500 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4302",
|
|||
|
|
productName: "Venusaur",
|
|||
|
|
setName: "Base Set",
|
|||
|
|
setCode: "BS",
|
|||
|
|
number: "15/102",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Grass",
|
|||
|
|
variant: "1st Edition",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 22000,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 19500 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 15000 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 10500 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 6500 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 3500 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4303",
|
|||
|
|
productName: "Espeon VMAX",
|
|||
|
|
setName: "Evolving Skies",
|
|||
|
|
setCode: "EVS",
|
|||
|
|
number: "205/203",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Psychic",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 2,
|
|||
|
|
purchasePrice: 7000,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 9200 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 7300 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 5200 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 3300 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 1600 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4304",
|
|||
|
|
productName: "Gengar VMAX",
|
|||
|
|
setName: "Fusion Strike",
|
|||
|
|
setCode: "FST",
|
|||
|
|
number: "271/264",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Psychic",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 5500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 4800 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 3800 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 2700 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 1700 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 900 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4305",
|
|||
|
|
productName: "Pikachu VMAX",
|
|||
|
|
setName: "Vivid Voltage",
|
|||
|
|
setCode: "VIV",
|
|||
|
|
number: "188/185",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Lightning",
|
|||
|
|
variant: "Rainbow Rare",
|
|||
|
|
qty: 3,
|
|||
|
|
purchasePrice: 3200,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 4100 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 3200 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 2300 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 1400 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 750 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4306",
|
|||
|
|
productName: "Alakazam",
|
|||
|
|
setName: "Base Set",
|
|||
|
|
setCode: "BS",
|
|||
|
|
number: "1/102",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Psychic",
|
|||
|
|
variant: "Shadowless",
|
|||
|
|
qty: 2,
|
|||
|
|
purchasePrice: 9500,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 12000 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 9300 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 6600 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 4100 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 2200 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4307",
|
|||
|
|
productName: "Sylveon VMAX",
|
|||
|
|
setName: "Evolving Skies",
|
|||
|
|
setCode: "EVS",
|
|||
|
|
number: "212/203",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Psychic",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 6800,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 8500 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 6700 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 4800 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 3000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 1500 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4308",
|
|||
|
|
productName: "Mew VMAX",
|
|||
|
|
setName: "Fusion Strike",
|
|||
|
|
setCode: "FST",
|
|||
|
|
number: "269/264",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Psychic",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 2,
|
|||
|
|
purchasePrice: 4200,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 5600 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 4400 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 3100 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 2000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 1000 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4309",
|
|||
|
|
productName: "Darkrai VSTAR",
|
|||
|
|
setName: "Astral Radiance",
|
|||
|
|
setCode: "ASR",
|
|||
|
|
number: "189/189",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Darkness",
|
|||
|
|
variant: "Gold",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 3800,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 3200 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 2500 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 1800 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 1100 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 600 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4310",
|
|||
|
|
productName: "Leafeon VSTAR",
|
|||
|
|
setName: "Pokémon GO",
|
|||
|
|
setCode: "PGO",
|
|||
|
|
number: "076/078",
|
|||
|
|
rarityName: "Ultra Rare",
|
|||
|
|
energyType: "Grass",
|
|||
|
|
variant: "Normal",
|
|||
|
|
qty: 4,
|
|||
|
|
purchasePrice: 1200,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 1800 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 1400 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 1000 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 600 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 300 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4311",
|
|||
|
|
productName: "Snorlax",
|
|||
|
|
setName: "Celebrations",
|
|||
|
|
setCode: "CEL",
|
|||
|
|
number: "005/025",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Colorless",
|
|||
|
|
variant: "Classic Collection",
|
|||
|
|
qty: 6,
|
|||
|
|
purchasePrice: 600,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 750 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 580 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 420 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 260 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 130 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4312",
|
|||
|
|
productName: "Articuno",
|
|||
|
|
setName: "Fossil",
|
|||
|
|
setCode: "FO",
|
|||
|
|
number: "2/62",
|
|||
|
|
rarityName: "Rare Holo",
|
|||
|
|
energyType: "Water",
|
|||
|
|
variant: "1st Edition",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 14000,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 11500 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 9000 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 6400 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 4000 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 2200 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4313",
|
|||
|
|
productName: "Radiant Charizard",
|
|||
|
|
setName: "Pokemon GO",
|
|||
|
|
setCode: "PGO",
|
|||
|
|
number: "011/078",
|
|||
|
|
rarityName: "Radiant Rare",
|
|||
|
|
energyType: "Fire",
|
|||
|
|
variant: "Normal",
|
|||
|
|
qty: 2,
|
|||
|
|
purchasePrice: 2800,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 4200 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 3300 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 2400 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 1500 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 800 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
productId: "4314",
|
|||
|
|
productName: "Giratina VSTAR",
|
|||
|
|
setName: "Lost Origin",
|
|||
|
|
setCode: "LOR",
|
|||
|
|
number: "196/196",
|
|||
|
|
rarityName: "Secret Rare",
|
|||
|
|
energyType: "Dragon",
|
|||
|
|
variant: "Alternate Art",
|
|||
|
|
qty: 1,
|
|||
|
|
purchasePrice: 5200,
|
|||
|
|
skus: [
|
|||
|
|
{ condition: "Near Mint", marketPrice: 7800 },
|
|||
|
|
{ condition: "Lightly Played", marketPrice: 6100 },
|
|||
|
|
{ condition: "Moderately Played", marketPrice: 4400 },
|
|||
|
|
{ condition: "Heavily Played", marketPrice: 2800 },
|
|||
|
|
{ condition: "Damaged", marketPrice: 1400 },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Helpers
|
|||
|
|
const nmPrice = (card: typeof inventory[0]) => (card.skus[0]?.marketPrice ?? 0) / 100;
|
|||
|
|
const nmPurchase = (card: typeof inventory[0]) => card.purchasePrice / 100;
|
|||
|
|
const gain = (card: typeof inventory[0]) => nmPrice(card) - nmPurchase(card);
|
|||
|
|
|
|||
|
|
const totalQty = inventory.reduce((s, c) => s + c.qty, 0);
|
|||
|
|
const totalValue = inventory.reduce((s, c) => s + nmPrice(c) * c.qty, 0);
|
|||
|
|
const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
<Layout title="Inventory Dashboard">
|
|||
|
|
<NavBar slot="navbar">
|
|||
|
|
<NavItems slot="navItems" />
|
|||
|
|
</NavBar>
|
|||
|
|
|
|||
|
|
<div class="row g-0" style="min-height: calc(100vh - 120px)" slot="page">
|
|||
|
|
<aside class="col-12 col-md-2 border-end border-secondary bg-dark p-3 d-flex flex-column gap-3">
|
|||
|
|
<div class="d-flex justify-content-between align-items-center">
|
|||
|
|
<h6 class="mb-0 text-uppercase text-secondary fw-bold ls-wide" style="letter-spacing:.08em">Catalogs</h6>
|
|||
|
|
<button
|
|||
|
|
class="btn btn-sm btn-info fs-7"
|
|||
|
|
title="New catalog"
|
|||
|
|
type="button"
|
|||
|
|
data-bs-toggle="modal"
|
|||
|
|
data-bs-target="#newCatalogModal"
|
|||
|
|
>+ New</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<ul id="catalogList" class="list-group list-group-flush">
|
|||
|
|
<li
|
|||
|
|
class="list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center justify-content-between active"
|
|||
|
|
data-catalog="all"
|
|||
|
|
role="button"
|
|||
|
|
style="cursor:pointer"
|
|||
|
|
>
|
|||
|
|
<span class="d-flex align-items-center gap-2">
|
|||
|
|
View all cards
|
|||
|
|
</span>
|
|||
|
|
<span class="badge rounded-pill text-bg-secondary small">{totalQty}</span>
|
|||
|
|
</li>
|
|||
|
|
|
|||
|
|
{["Case Cards", "Japanese Singles", "Bulk"].map((name) => (
|
|||
|
|
<li
|
|||
|
|
class="ms-2 list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center gap-2"
|
|||
|
|
data-catalog={name}
|
|||
|
|
role="button"
|
|||
|
|
style="cursor:pointer"
|
|||
|
|
>
|
|||
|
|
{name}
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<div class="mt-auto pt-3 border-top border-secondary small text-secondary">
|
|||
|
|
<div class="d-flex justify-content-between mb-1"><span>Total Cards</span><span class="text-light fw-semibold">{totalQty}</span></div>
|
|||
|
|
<div class="d-flex justify-content-between mb-1"><span>Market Value</span><span class="text-success fw-semibold">${totalValue.toFixed(0)}</span></div>
|
|||
|
|
<div class="d-flex justify-content-between"><span>Profit/Loss</span><span class={`fw-semibold ${totalGain >= 0 ? "text-success" : "text-danger"}`}>{totalGain >= 0 ? "+" : ""}${Math.abs(totalGain).toFixed(0)}</span></div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<main class="col-12 col-md-10 p-4">
|
|||
|
|
<div class="d-flex flex-wrap gap-2 align-items-center mb-4">
|
|||
|
|
<div class="d-flex align-items-center gap-1">
|
|||
|
|
<button id="btnGrid" type="button" class="btn btn-sm btn-link text-secondary px-1 view-toggle-btn active" title="Images view">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|||
|
|
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3A1.5 1.5 0 0 1 15 10.5v3A1.5 1.5 0 0 1 13.5 15h-3A1.5 1.5 0 0 1 9 13.5v-3z"/>
|
|||
|
|
</svg>
|
|||
|
|
<span class="small ms-1">Images</span>
|
|||
|
|
</button>
|
|||
|
|
<button id="btnTable" type="button" class="btn btn-sm btn-link text-secondary px-1 view-toggle-btn" title="List view">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|||
|
|
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
|||
|
|
</svg>
|
|||
|
|
<span class="small ms-1">List</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="vr opacity-25 mx-1"></div>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="btn btn-sm btn-info"
|
|||
|
|
data-bs-toggle="modal"
|
|||
|
|
data-bs-target="#addCardModal"
|
|||
|
|
>+ Add Card</button>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="btn btn-sm btn-outline-light"
|
|||
|
|
data-bs-toggle="modal"
|
|||
|
|
data-bs-target="#bulkImportModal"
|
|||
|
|
>⬆ CSV Import</button>
|
|||
|
|
|
|||
|
|
<div class="ms-auto position-relative">
|
|||
|
|
<input
|
|||
|
|
id="inventorySearch"
|
|||
|
|
class="form-control form-control-sm bg-dark text-light border-secondary"
|
|||
|
|
placeholder="Search inventory…"
|
|||
|
|
style="min-width:200px; padding-right:2rem"
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
id="clearSearch"
|
|||
|
|
type="button"
|
|||
|
|
class="btn btn-sm p-0 position-absolute top-50 end-0 translate-middle-y me-2 text-secondary d-none"
|
|||
|
|
style="line-height:1"
|
|||
|
|
aria-label="Clear search"
|
|||
|
|
>✕</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="inventoryView">
|
|||
|
|
<div id="gridView" class="row g-4 row-cols-2 row-cols-md-3 row-cols-xl-4 row-cols-xxxl-5">
|
|||
|
|
{inventory.map(card => {
|
|||
|
|
const market = nmPrice(card);
|
|||
|
|
const purchase = nmPurchase(card);
|
|||
|
|
const diff = market - purchase;
|
|||
|
|
const pct = purchase > 0 ? (diff / purchase) * 100 : 0;
|
|||
|
|
const isGain = diff >= 0;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div class="col">
|
|||
|
|
<article class="inv-grid-card">
|
|||
|
|
<div
|
|||
|
|
class="card-trigger position-relative inv-grid-media"
|
|||
|
|
data-card-id={card.productId}
|
|||
|
|
data-bs-toggle="modal"
|
|||
|
|
data-bs-target="#cardModal"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
class="image-grow rounded-4 card-image"
|
|||
|
|
data-energy={card.energyType}
|
|||
|
|
data-rarity={card.rarityName}
|
|||
|
|
data-variant={card.variant}
|
|||
|
|
data-name={card.productName}
|
|||
|
|
>
|
|||
|
|
<img
|
|||
|
|
src={`/cards/${card.productId}.jpg`}
|
|||
|
|
alt={card.productName}
|
|||
|
|
loading="lazy"
|
|||
|
|
decoding="async"
|
|||
|
|
class="img-fluid rounded-4 w-100"
|
|||
|
|
onerror="this.onerror=null;this.src='/cards/default.jpg';this.closest('.image-grow')?.setAttribute('data-default','true')"
|
|||
|
|
/>
|
|||
|
|
<span class="position-absolute top-50 start-0 d-inline medium-icon" style="z-index:4">
|
|||
|
|
<FirstEditionIcon edition={card.variant} />
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inv-grid-body">
|
|||
|
|
<div class="inv-grid-main">
|
|||
|
|
<div class="h5">{card.productName}</div>
|
|||
|
|
<div class="inv-grid-meta">
|
|||
|
|
<div class="p">{card.setName}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inv-grid-price">
|
|||
|
|
<div class={`inv-grid-trend ${isGain ? "up" : "down"}`}>
|
|||
|
|
<span class="inv-grid-arrow">{isGain ? "▲" : "▼"}</span>
|
|||
|
|
<span class="h6">${market.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class={`inv-grid-delta fs-6 ${isGain ? "up" : "down"}`}>
|
|||
|
|
{isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)} ({isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}%)
|
|||
|
|
</div>
|
|||
|
|
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
|||
|
|
<div class="d-flex align-items-center gap-2">
|
|||
|
|
<div class="btn-group" role="group" aria-label="Quantity controls">
|
|||
|
|
<button type="button" class="btn btn-outline-secondary btn-sm">−</button>
|
|||
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" tabindex="-1">2</button>
|
|||
|
|
<button type="button" class="btn btn-outline-secondary btn-sm">+</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</article>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="tableView" style="display:none">
|
|||
|
|
<div class="inv-list-wrap">
|
|||
|
|
<table class="table align-middle mb-0 inv-list-table">
|
|||
|
|
<tbody id="inventoryRows">
|
|||
|
|
{inventory.map(card => {
|
|||
|
|
const market = nmPrice(card);
|
|||
|
|
const purchase = nmPurchase(card);
|
|||
|
|
const diff = market - purchase;
|
|||
|
|
const pct = purchase > 0 ? (diff / purchase) * 100 : 0;
|
|||
|
|
const isGain = diff >= 0;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<tr class="inv-list-row">
|
|||
|
|
<td class="inv-list-cardcell">
|
|||
|
|
<div class="inv-list-card">
|
|||
|
|
<div
|
|||
|
|
class="inv-list-thumb card-trigger"
|
|||
|
|
data-card-id={card.productId}
|
|||
|
|
data-bs-toggle="modal"
|
|||
|
|
data-bs-target="#cardModal"
|
|||
|
|
>
|
|||
|
|
<img
|
|||
|
|
src={`/cards/${card.productId}.jpg`}
|
|||
|
|
alt={card.productName}
|
|||
|
|
loading="lazy"
|
|||
|
|
decoding="async"
|
|||
|
|
onerror="this.onerror=null;this.src='/cards/default.jpg';"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inv-list-info">
|
|||
|
|
<div
|
|||
|
|
class="inv-list-name"
|
|||
|
|
data-card-id={card.productId}
|
|||
|
|
data-bs-toggle="modal"
|
|||
|
|
data-bs-target="#cardModal"
|
|||
|
|
style="cursor:pointer"
|
|||
|
|
>
|
|||
|
|
{card.productName}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inv-list-meta">
|
|||
|
|
<div class="inv-list-setlink">{card.setName}</div>
|
|||
|
|
<div>{card.rarityName}</div>
|
|||
|
|
<div>{card.number}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inv-list-condition">
|
|||
|
|
<span>Near Mint</span>
|
|||
|
|
<span>•</span>
|
|||
|
|
<span>{card.variant !== "Normal" ? card.variant : "Holofoil"}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="inv-list-right">
|
|||
|
|
<div class={`inv-list-price-line ${isGain ? "up" : "down"}`}>
|
|||
|
|
<span class="inv-grid-arrow small">{isGain ? "▲" : "▼"}</span>
|
|||
|
|
<span class="inv-list-price">${market.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class={`inv-list-delta ${isGain ? "up" : "down"}`}>
|
|||
|
|
{isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)} ({isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}%)
|
|||
|
|
</div>
|
|||
|
|
<div class="inv-list-qty">Qty: {card.qty}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
<div class="text-secondary small mt-2 ps-1" id="rowCount"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="modal fade" id="newCatalogModal" tabindex="-1" aria-labelledby="newCatalogLabel" aria-modal="true" role="dialog">
|
|||
|
|
<div class="modal-dialog modal-dialog-centered">
|
|||
|
|
<div class="modal-content bg-dark text-light border border-secondary">
|
|||
|
|
<div class="modal-header border-secondary">
|
|||
|
|
<h5 class="modal-title" id="newCatalogLabel">Create Catalog</h5>
|
|||
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<label class="form-label small text-secondary text-uppercase fw-semibold" for="catalogNameInput">Catalog Name</label>
|
|||
|
|
<input id="catalogNameInput" type="text" class="form-control bg-dark-subtle text-light border-secondary" placeholder="e.g. Japanese Holos" />
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer border-secondary">
|
|||
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|||
|
|
<button type="button" class="btn btn-success" id="createCatalogBtn">Create</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="modal fade" id="bulkImportModal" tabindex="-1" aria-labelledby="bulkImportLabel" aria-modal="true" role="dialog">
|
|||
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|||
|
|
<div class="modal-content bg-dark text-light border border-secondary">
|
|||
|
|
<div class="modal-header border-secondary">
|
|||
|
|
<h5 class="modal-title" id="bulkImportLabel">Bulk CSV Import</h5>
|
|||
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<p class="small text-secondary mb-3">
|
|||
|
|
Upload a CSV exported from Collectr, TCGPlayer, or any marketplace. Columns: <code>name, set, condition, qty, price, market</code>.
|
|||
|
|
</p>
|
|||
|
|
<label class="form-label small text-secondary text-uppercase fw-semibold" for="csvFileInput">Choose File</label>
|
|||
|
|
<input id="csvFileInput" type="file" accept=".csv" class="form-control bg-dark-subtle text-light border-secondary" />
|
|||
|
|
<div id="csvPreview" class="mt-3 d-none">
|
|||
|
|
<p class="small text-secondary fw-semibold mb-1">Preview</p>
|
|||
|
|
<div class="border border-secondary rounded p-2 small text-secondary" id="csvPreviewContent">—</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer border-secondary">
|
|||
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|||
|
|
<button type="button" class="btn btn-success" id="csvUploadBtn">Upload & Preview</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="modal fade" id="inventoryEditModal" tabindex="-1" aria-labelledby="inventoryEditLabel" aria-modal="true" role="dialog">
|
|||
|
|
<div class="modal-dialog modal-dialog-centered">
|
|||
|
|
<div class="modal-content bg-dark text-light border border-secondary">
|
|||
|
|
<div class="modal-header border-secondary">
|
|||
|
|
<h5 class="modal-title" id="inventoryEditLabel">Edit Inventory</h5>
|
|||
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body" id="inventoryEditBody">
|
|||
|
|
<p class="text-secondary small">Select a card to edit its quantity and purchase price.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer border-secondary">
|
|||
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|||
|
|
<button type="button" class="btn btn-success">Save</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="modal fade" id="addCardModal" tabindex="-1" aria-labelledby="addCardLabel" aria-modal="true" role="dialog">
|
|||
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|||
|
|
<div class="modal-content bg-dark text-light border border-secondary">
|
|||
|
|
<div class="modal-header border-secondary">
|
|||
|
|
<h5 class="modal-title" id="addCardLabel">Add Card</h5>
|
|||
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<input type="text" class="form-control bg-dark-subtle text-light border-secondary mb-3" placeholder="Search card name…" id="addCardSearch" />
|
|||
|
|
<p class="text-secondary small">Search results will appear here. Connect to your card database API to enable live search.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer border-secondary">
|
|||
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|||
|
|
<button type="button" class="btn btn-primary" disabled>Add Selected</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Footer slot="footer" />
|
|||
|
|
</Layout>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
|
|||
|
|
.view-toggle-btn {
|
|||
|
|
text-decoration: none;
|
|||
|
|
color: var(--bs-secondary-color) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.view-toggle-btn.active {
|
|||
|
|
color: var(--bs-body-color) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.view-toggle-btn.active svg,
|
|||
|
|
.view-toggle-btn:hover svg {
|
|||
|
|
color: var(--bs-body-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#catalogList .list-group-item.active {
|
|||
|
|
background-color: rgba(var(--bs-danger-rgb), .15) !important;
|
|||
|
|
color: rgba(var(--bs-danger-rgb), 1) !important;
|
|||
|
|
border-left: 2px solid var(--bs-danger) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#gridView {
|
|||
|
|
row-gap: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-card {
|
|||
|
|
position: relative;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-media {
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-body {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 1rem;
|
|||
|
|
padding: .85rem .15rem 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-main {
|
|||
|
|
min-width: 0;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-title {
|
|||
|
|
font-size: 1.9rem;
|
|||
|
|
line-height: 1.05;
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin-bottom: .4rem;
|
|||
|
|
color: var(--bs-emphasis-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: .2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-submeta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: .35rem;
|
|||
|
|
font-size: .9rem;
|
|||
|
|
color: var(--bs-secondary-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-price {
|
|||
|
|
min-width: 112px;
|
|||
|
|
text-align: right;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
color: #f3f3f3 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-trend,
|
|||
|
|
.inv-list-price-line {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
gap: .35rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
line-height: 1.1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-value,
|
|||
|
|
.inv-list-price {
|
|||
|
|
font-size: 1.15rem;
|
|||
|
|
color: #111;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-trend.up .inv-grid-arrow,
|
|||
|
|
.inv-list-price-line.up .inv-grid-arrow {
|
|||
|
|
color: #2e7d32;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-trend.down,
|
|||
|
|
.inv-list-price-line.down {
|
|||
|
|
color: #c62828;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-delta,
|
|||
|
|
.inv-list-delta {
|
|||
|
|
margin-top: .2rem;
|
|||
|
|
font-size: .86rem;
|
|||
|
|
line-height: 1.15;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-delta.up,
|
|||
|
|
.inv-list-delta.up {
|
|||
|
|
color: #2e7d32;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-delta.down,
|
|||
|
|
.inv-list-delta.down {
|
|||
|
|
color: #c62828;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-qty,
|
|||
|
|
.inv-list-qty {
|
|||
|
|
margin-top: .35rem;
|
|||
|
|
font-size: .9rem;
|
|||
|
|
color: #1ea7a1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-cart,
|
|||
|
|
.inv-list-cart {
|
|||
|
|
position: absolute;
|
|||
|
|
right: .35rem;
|
|||
|
|
bottom: .1rem;
|
|||
|
|
width: 2.15rem;
|
|||
|
|
height: 2.15rem;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
border: 1px solid #4cb7b3;
|
|||
|
|
background: transparent;
|
|||
|
|
color: #38a9a5;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
transition: .15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-cart:hover,
|
|||
|
|
.inv-list-cart:hover {
|
|||
|
|
background: rgba(76, 183, 179, .08);
|
|||
|
|
color: #2a9a96;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#tableView {
|
|||
|
|
background: transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-wrap {
|
|||
|
|
border-radius: 0;
|
|||
|
|
overflow: visible;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-table {
|
|||
|
|
--bs-table-bg: transparent;
|
|||
|
|
--bs-table-hover-bg: transparent;
|
|||
|
|
--bs-table-color: inherit;
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-table tbody,
|
|||
|
|
.inv-list-table tr,
|
|||
|
|
.inv-list-table td {
|
|||
|
|
border: 0 !important;
|
|||
|
|
background: transparent !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-row + .inv-list-row .inv-list-cardcell {
|
|||
|
|
border-top: 1px solid rgba(0, 0, 0, .08) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-cardcell {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-card {
|
|||
|
|
position: relative;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 1rem;
|
|||
|
|
min-height: 116px;
|
|||
|
|
padding: .8rem 4.5rem .8rem .35rem;
|
|||
|
|
background: #f3f3f3;
|
|||
|
|
border: 1px solid rgba(0, 0, 0, .08);
|
|||
|
|
border-radius: .35rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-thumb {
|
|||
|
|
width: 70px;
|
|||
|
|
flex: 0 0 70px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-thumb img {
|
|||
|
|
width: 100%;
|
|||
|
|
display: block;
|
|||
|
|
border-radius: .25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-info {
|
|||
|
|
min-width: 0;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-name {
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #111827;
|
|||
|
|
margin-bottom: .35rem;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
max-width: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-name:hover {
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: .1rem;
|
|||
|
|
font-size: .95rem;
|
|||
|
|
color: #4b5563;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-setlink {
|
|||
|
|
color: #5b6f8f;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
text-underline-offset: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-condition {
|
|||
|
|
margin-top: .35rem;
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: .35rem;
|
|||
|
|
font-size: .95rem;
|
|||
|
|
color: #2b7a78;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-right {
|
|||
|
|
margin-left: auto;
|
|||
|
|
min-width: 140px;
|
|||
|
|
text-align: right;
|
|||
|
|
padding-right: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.inv-grid-body {
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: .5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-price {
|
|||
|
|
min-width: 0;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-grid-trend {
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-card {
|
|||
|
|
align-items: flex-start;
|
|||
|
|
padding-right: 3.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inv-list-right {
|
|||
|
|
min-width: 0;
|
|||
|
|
padding-right: .25rem;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import * as bootstrap from "bootstrap";
|
|||
|
|
|
|||
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|||
|
|
document.querySelectorAll(".modal").forEach((el) => {
|
|||
|
|
bootstrap.Modal.getOrCreateInstance(el);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.querySelectorAll("#catalogList [data-catalog]").forEach((item) => {
|
|||
|
|
item.addEventListener("click", () => {
|
|||
|
|
document.querySelectorAll("#catalogList [data-catalog]").forEach((i) => i.classList.remove("active"));
|
|||
|
|
item.classList.add("active");
|
|||
|
|
|
|||
|
|
const catalog = item.dataset.catalog ?? "all";
|
|||
|
|
|
|||
|
|
document.querySelectorAll("#gridView .col").forEach((col) => {
|
|||
|
|
col.style.display = catalog === "all" ? "" : "none";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.querySelectorAll("#inventoryRows tr").forEach((row) => {
|
|||
|
|
row.style.display = catalog === "all" ? "" : "none";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
updateRowCount();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const gridView = document.getElementById("gridView");
|
|||
|
|
const tableView = document.getElementById("tableView");
|
|||
|
|
const btnGrid = document.getElementById("btnGrid");
|
|||
|
|
const btnTable = document.getElementById("btnTable");
|
|||
|
|
const rowCount = document.getElementById("rowCount");
|
|||
|
|
const tbody = document.getElementById("inventoryRows");
|
|||
|
|
|
|||
|
|
function showGrid() {
|
|||
|
|
gridView.style.display = "";
|
|||
|
|
tableView.style.display = "none";
|
|||
|
|
btnGrid.classList.add("active");
|
|||
|
|
btnTable.classList.remove("active");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showTable() {
|
|||
|
|
gridView.style.display = "none";
|
|||
|
|
tableView.style.display = "";
|
|||
|
|
btnGrid.classList.remove("active");
|
|||
|
|
btnTable.classList.add("active");
|
|||
|
|
updateRowCount();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
btnGrid?.addEventListener("click", showGrid);
|
|||
|
|
btnTable?.addEventListener("click", showTable);
|
|||
|
|
|
|||
|
|
const searchInput = document.getElementById("inventorySearch");
|
|||
|
|
const clearBtn = document.getElementById("clearSearch");
|
|||
|
|
|
|||
|
|
let searchTimer;
|
|||
|
|
|
|||
|
|
searchInput?.addEventListener("input", () => {
|
|||
|
|
clearTimeout(searchTimer);
|
|||
|
|
searchTimer = setTimeout(() => {
|
|||
|
|
const term = searchInput.value.toLowerCase();
|
|||
|
|
clearBtn.classList.toggle("d-none", !term);
|
|||
|
|
|
|||
|
|
tbody?.querySelectorAll("tr").forEach((row) => {
|
|||
|
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "" : "none";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
updateRowCount();
|
|||
|
|
}, 120);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
clearBtn?.addEventListener("click", () => {
|
|||
|
|
searchInput.value = "";
|
|||
|
|
clearBtn.classList.add("d-none");
|
|||
|
|
tbody?.querySelectorAll("tr").forEach((r) => {
|
|||
|
|
r.style.display = "";
|
|||
|
|
});
|
|||
|
|
updateRowCount();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function updateRowCount() {
|
|||
|
|
if (!rowCount || !tbody) return;
|
|||
|
|
const visible = [...tbody.querySelectorAll("tr")].filter((r) => r.style.display !== "none").length;
|
|||
|
|
rowCount.textContent = `Showing ${visible} card${visible !== 1 ? "s" : ""}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateRowCount();
|
|||
|
|
|
|||
|
|
document.getElementById("createCatalogBtn")?.addEventListener("click", () => {
|
|||
|
|
const input = document.getElementById("catalogNameInput");
|
|||
|
|
const name = input.value.trim();
|
|||
|
|
if (!name) {
|
|||
|
|
input.focus();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const li = document.createElement("li");
|
|||
|
|
li.className =
|
|||
|
|
"list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center gap-2";
|
|||
|
|
li.setAttribute("data-catalog", name);
|
|||
|
|
li.setAttribute("role", "button");
|
|||
|
|
li.style.cursor = "pointer";
|
|||
|
|
li.innerHTML = `<span class="text-secondary" style="font-size:.7rem">▸</span>${name}`;
|
|||
|
|
|
|||
|
|
li.addEventListener("click", () => {
|
|||
|
|
document.querySelectorAll("#catalogList [data-catalog]").forEach((i) => i.classList.remove("active"));
|
|||
|
|
li.classList.add("active");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById("catalogList")?.appendChild(li);
|
|||
|
|
|
|||
|
|
input.value = "";
|
|||
|
|
bootstrap.Modal.getInstance(document.getElementById("newCatalogModal"))?.hide();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById("csvFileInput")?.addEventListener("change", (e) => {
|
|||
|
|
const file = e.target.files?.[0];
|
|||
|
|
if (!file) return;
|
|||
|
|
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (ev) => {
|
|||
|
|
const lines = ev.target.result.split("\n").slice(0, 4);
|
|||
|
|
const preview = document.getElementById("csvPreviewContent");
|
|||
|
|
preview.textContent = lines.join("\n");
|
|||
|
|
document.getElementById("csvPreview")?.classList.remove("d-none");
|
|||
|
|
};
|
|||
|
|
reader.readAsText(file);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
</script>
|