Deck status (Idea/Building/Built/Retired) and pulled-card tracking

Only building and built decks now claim owned copies, so several idea
decks can share one copy without an over-assignment warning. Deck cards
track how many copies are physically pulled: physical decks list cards
to pull with their source (a storage box, or another deck to take a copy
from), and idea decks split cards into available / in other decks / not
owned. The deck list shows status chips with to-pull counts, filters by
status, and collapses retired decks. Status and pulled counts sync,
which bumps the library document to version 4.

Existing decks migrate as ideas.

Closes #25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:35:41 +00:00
parent eb342f5fbb
commit 99a579a697
20 changed files with 1087 additions and 103 deletions

View File

@@ -25,7 +25,12 @@ export interface CardRef {
export interface LibraryCard extends CardRef {
quantity_owned: number;
decks: string[];
/** The subset of `decks` that physically exist (building or built). */
physical_decks: string[];
/** Copies claimed by building/built decks (idea/retired decks claim none). */
quantity_assigned: number;
/** Copies physically pulled into decks; the rest are at `location`. */
quantity_in_decks: number;
location: string;
}
@@ -46,8 +51,41 @@ export interface DeckEntry extends CardRef {
/** The printing whose art this slot shows; `image_uri` already reflects it. */
preferred_scryfall_id: string | null;
partner: Partner | null;
/** Copies physically in this deck; meaningful for building/built decks. */
quantity_pulled: number;
/** Owned copies not claimed by any *other* building/built deck. */
quantity_spare: number;
/** Owned copies not pulled into any deck — still in their box. */
quantity_in_box: number;
/** The *other* building/built decks claiming copies; those with copies
* pulled are where to take one from when the box is empty. */
claimed_by: DeckHolding[];
/** Where the card's library copies are stored ("" if unset). */
location: string;
}
export interface DeckHolding {
deck_id: number;
name: string;
/** Copies the deck's main board lists. */
quantity: number;
/** Of those, copies physically in the deck. */
pulled: number;
}
/** Where a deck stands physically. Building and built decks hold cards and
* claim owned copies; ideas and retired decks are just lists. */
export type DeckStatus = "idea" | "building" | "built" | "retired";
export const DECK_STATUSES: { value: DeckStatus; label: string }[] = [
{ value: "idea", label: "Idea" },
{ value: "building", label: "Building" },
{ value: "built", label: "Built" },
{ value: "retired", label: "Retired" },
];
export const isPhysical = (s: DeckStatus) => s === "building" || s === "built";
/** One printing of a card, for the printing picker and copy labels. */
export interface PrintingRef {
scryfall_id: string;
@@ -82,6 +120,9 @@ export interface LibraryCopy {
export type PreviewCard = CardRef & {
quantity_owned?: number;
decks?: string[];
physical_decks?: string[];
quantity_in_decks?: number;
location?: string;
preferred_scryfall_id?: string | null;
};
@@ -107,6 +148,7 @@ export interface Deck {
notes: string;
/** The card chosen to headline the deck in the deck list; null = automatic. */
cover_oracle_id: string | null;
status: DeckStatus;
}
export interface DeckImportResult {
@@ -497,6 +539,17 @@ export const api = {
invoke<Deck>("duplicate_deck", { deckId, newName }),
setDeckFormat: (deckId: number, format: string) =>
invoke<void>("set_deck_format", { deckId, format }),
/** Change a deck's status. Leaving building/built returns pulled copies to
* their boxes; entering it with `assembled` marks the copies in the box as
* already in the deck. */
setDeckStatus: (deckId: number, status: DeckStatus, assembled = false) =>
invoke<void>("set_deck_status", { deckId, status, assembled }),
/** Record copies physically in a deck; returns the (clamped) count stored. */
setDeckCardPulled: (deckId: number, oracleId: string, pulled: number) =>
invoke<number>("set_deck_card_pulled", { deckId, oracleId, pulled }),
/** Move one pulled copy between decks; false if there was nothing to move. */
takeDeckCardCopy: (fromDeckId: number, toDeckId: number, oracleId: string) =>
invoke<boolean>("take_deck_card_copy", { fromDeckId, toDeckId, oracleId }),
/** Choose the card headlining the deck in the deck list; null = automatic. */
setDeckCover: (deckId: number, oracleId: string | null) =>
invoke<void>("set_deck_cover", { deckId, oracleId }),