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

@@ -188,12 +188,19 @@ pub struct LibraryCard {
pub quantity_owned: i64,
/// Names of decks this card is currently assigned to.
pub decks: Vec<String>,
/// Total copies assigned across all decks. If it exceeds `quantity_owned`,
/// the physical copies are over-committed.
/// Copies claimed by building/built decks' main boards (idea and retired
/// decks claim nothing). If it exceeds `quantity_owned`, the physical
/// copies are over-committed.
pub quantity_assigned: i64,
/// Free-text physical storage location (e.g. "Box 1"). Meaningful mainly for
/// cards not currently in a deck.
pub location: String,
/// The subset of `decks` that physically exist (building or built).
#[serde(default)]
pub physical_decks: Vec<String>,
/// Copies physically pulled into decks; the rest are at `location`.
#[serde(default)]
pub quantity_in_decks: i64,
}
/// A card in a deck, whether or not the user owns it.
@@ -218,6 +225,75 @@ pub struct DeckEntry {
/// zone with a second commander.
#[serde(default)]
pub partner: Option<crate::partner::Partner>,
/// Copies physically in this deck; the rest are still to be pulled. Only
/// meaningful for building/built decks.
#[serde(default)]
pub quantity_pulled: i64,
/// Owned copies not claimed by any *other* building/built deck.
#[serde(default)]
pub quantity_spare: i64,
/// Owned copies not pulled into any deck — still in their box.
#[serde(default)]
pub quantity_in_box: i64,
/// The *other* building/built decks claiming copies — those with copies
/// pulled are where to take one from when none are left in the box.
#[serde(default)]
pub claimed_by: Vec<DeckHolding>,
/// Where the card's library copies are stored (e.g. "Box 1").
#[serde(default)]
pub location: String,
}
/// A building/built deck's claim on a card's copies.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeckHolding {
pub deck_id: i64,
pub name: String,
/// Copies the deck's main board lists.
pub quantity: i64,
/// Of those, copies physically in the deck.
pub pulled: i64,
}
/// Where a deck stands physically. Only building and built decks hold cards
/// and claim owned copies; ideas and retired decks are just lists.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DeckStatus {
#[default]
Idea,
Building,
Built,
Retired,
}
/// SQL list of the statuses for which [`DeckStatus::is_physical`] holds.
pub const PHYSICAL_STATUSES_SQL: &str = "('building', 'built')";
impl DeckStatus {
pub fn as_str(self) -> &'static str {
match self {
DeckStatus::Idea => "idea",
DeckStatus::Building => "building",
DeckStatus::Built => "built",
DeckStatus::Retired => "retired",
}
}
/// Unknown values (say, from a newer build) read as an idea, which holds
/// nothing — the safe direction to be wrong in.
pub fn parse(s: &str) -> Self {
match s {
"building" => DeckStatus::Building,
"built" => DeckStatus::Built,
"retired" => DeckStatus::Retired,
_ => DeckStatus::Idea,
}
}
pub fn is_physical(self) -> bool {
matches!(self, DeckStatus::Building | DeckStatus::Built)
}
}
/// One printing of a card, for the printing picker and copy labels.
@@ -272,6 +348,8 @@ pub struct Deck {
/// The card whose art headlines the deck in the deck list, if chosen;
/// `None` leaves the pick to the UI.
pub cover_oracle_id: Option<String>,
#[serde(default)]
pub status: DeckStatus,
}
/// Aggregate stats about the owned library, for a dashboard-style summary.