Collection Management

Learn how to manage your beer collection with advanced features like inventory tracking, consumption history, and storage management.

Collection Management

BrewHoard provides comprehensive tools for managing your beer collection, from adding new acquisitions to tracking consumption and storage details.

Adding Beers to Your Collection

You can add beers manually or through the scanner feature. Each beer entry includes detailed information about the beer, acquisition details, and storage tracking.

Manual Beer Addition

Svelte
<!-- src/components/collection/AddBeerForm.svelte -->
<script>
  let { onBeerAdded } = $props();
  
  let beerData = $state({
    name: '',
    brewery: '',
    style: '',
    abv: 0,
    ibu: 0,
    quantity: 1,
    acquisitionDate: new Date().toISOString().split('T')[0],
    purchasePrice: 0,
    storageLocation: '',
    bestBeforeDate: '',
    batchNumber: ''
  });
  
  let isSubmitting = $state(false);
  
  async function handleSubmit() {
    isSubmitting = true;
    try {
      const response = await fetch('/api/beers', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(beerData)
      });
      
      if (response.ok) {
        onBeerAdded?.();
        // Reset form
        beerData = {
          name: '',
          brewery: '',
          style: '',
          abv: 0,
          ibu: 0,
          quantity: 1,
          acquisitionDate: new Date().toISOString().split('T')[0],
          purchasePrice: 0,
          storageLocation: '',
          bestBeforeDate: '',
          batchNumber: ''
        };
      }
    } finally {
      isSubmitting = false;
    }
  }
</script>

<form onsubmit|preventDefault={handleSubmit}>
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
    <div>
      <label for="name">Beer Name</label>
      <input id="name" bind:value={beerData.name} required />
    </div>
    
    <div>
      <label for="brewery">Brewery</label>
      <input id="brewery" bind:value={beerData.brewery} required />
    </div>
    
    <div>
      <label for="style">Style</label>
      <input id="style" bind:value={beerData.style} />
    </div>
    
    <div>
      <label for="abv">ABV (%)</label>
      <input id="abv" type="number" step="0.1" bind:value={beerData.abv} />
    </div>
    
    <div>
      <label for="quantity">Quantity</label>
      <input id="quantity" type="number" bind:value={beerData.quantity} min="1" />
    </div>
    
    <div>
      <label for="acquisitionDate">Acquisition Date</label>
      <input id="acquisitionDate" type="date" bind:value={beerData.acquisitionDate} />
    </div>
    
    <div>
      <label for="purchasePrice">Purchase Price</label>
      <input id="purchasePrice" type="number" step="0.01" bind:value={beerData.purchasePrice} />
    </div>
    
    <div>
      <label for="storageLocation">Storage Location</label>
      <input id="storageLocation" bind:value={beerData.storageLocation} placeholder="Fridge, cellar, etc." />
    </div>
    
    <div>
      <label for="bestBeforeDate">Best Before Date</label>
      <input id="bestBeforeDate" type="date" bind:value={beerData.bestBeforeDate} />
    </div>
    
    <div>
      <label for="batchNumber">Batch Number</label>
      <input id="batchNumber" bind:value={beerData.batchNumber} />
    </div>
  </div>
  
  <button type="submit" disabled={isSubmitting}>
    {#if isSubmitting}
      Adding Beer...
    {:else}
      Add to Collection
    {/if}
  </button>
</form>

Inventory Tracking

Track your beer quantities and monitor stock levels across different storage locations.

Inventory Management Component

Svelte
<!-- src/components/collection/InventoryTracker.svelte -->
<script>
  let { beers } = $props();
  
  // Group beers by storage location
  let inventoryByLocation = $derived(() => {
    const grouped = {};
    for (const beer of beers) {
      const location = beer.storageLocation || 'Unspecified';
      if (!grouped[location]) {
        grouped[location] = [];
      }
      grouped[location].push(beer);
    }
    return grouped;
  });
  
  // Calculate total value and quantity
  let totalStats = $derived(() => {
    let totalQuantity = 0;
    let totalValue = 0;
    
    for (const beer of beers) {
      totalQuantity += beer.quantity || 1;
      totalValue += (beer.purchasePrice || 0) * (beer.quantity || 1);
    }
    
    return { totalQuantity, totalValue };
  });
</script>

<div class="inventory-dashboard">
  <div class="stats-grid">
    <div class="stat-card">
      <h3>Total Beers</h3>
      <p class="stat-value">{totalStats.totalQuantity}</p>
    </div>
    
    <div class="stat-card">
      <h3>Total Value</h3>
      <p class="stat-value">${totalStats.totalValue.toFixed(2)}</p>
    </div>
  </div>
  
  {#each Object.entries(inventoryByLocation) as [location, locationBeers]}
    <div class="location-section">
      <h4>{location}</h4>
      <div class="beer-grid">
        {#each locationBeers as beer}
          <div class="beer-card">
            <h5>{beer.name}</h5>
            <p>{beer.brewery}</p>
            <p>Quantity: {beer.quantity}</p>
            {#if beer.bestBeforeDate}
              <p class="expiry {new Date(beer.bestBeforeDate) < new Date() ? 'expired' : ''}">
                Best before: {new Date(beer.bestBeforeDate).toLocaleDateString()}
              </p>
            {/if}
          </div>
        {/each}
      </div>
    </div>
  {/each}
</div>

<style>
  .inventory-dashboard {
    padding: 1rem;
  }
  
  .stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin-bottom: 2rem;
  }
  
  .stat-card {
    background: var(--card-bg);
    padding: 1rem;
    border-radius: 8px;
    text-align: center;
  }
  
  .stat-value {
    font-size: 2rem;
    font-weight: bold;
    color: var(--primary);
  }
  
  .location-section {
    margin-bottom: 2rem;
  }
  
  .beer-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 1rem;
  }
  
  .beer-card {
    background: var(--card-bg);
    padding: 1rem;
    border-radius: 8px;
  }
  
  .expiry.expired {
    color: var(--error);
  }
</style>

Consumption History

Track when and how you consume beers from your collection, including serving types and occasions.

Consumption Logging

Svelte
<!-- src/components/collection/ConsumptionLogger.svelte -->
<script>
  let { beer, onConsumed } = $props();
  
  let consumptionData = $state({
    date: new Date().toISOString().split('T')[0],
    servingType: 'bottle',
    occasion: '',
    notes: ''
  });
  
  let isLogging = $state(false);
  
  async function logConsumption() {
    isLogging = true;
    try {
      const response = await fetch(`/api/beers/${beer.id}/consume`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(consumptionData)
      });
      
      if (response.ok) {
        onConsumed?.();
      }
    } finally {
      isLogging = false;
    }
  }
</script>

<div class="consumption-logger">
  <h4>Log Consumption: {beer.name}</h4>
  
  <form onsubmit|preventDefault={logConsumption}>
    <div class="form-grid">
      <div>
        <label for="date">Consumption Date</label>
        <input id="date" type="date" bind:value={consumptionData.date} required />
      </div>
      
      <div>
        <label for="servingType">Serving Type</label>
        <select id="servingType" bind:value={consumptionData.servingType}>
          <option value="bottle">Bottle</option>
          <option value="can">Can</option>
          <option value="draft">Draft</option>
          <option value="cask">Cask</option>
        </select>
      </div>
      
      <div>
        <label for="occasion">Occasion</label>
        <input id="occasion" bind:value={consumptionData.occasion} placeholder="Dinner, party, etc." />
      </div>
      
      <div class="full-width">
        <label for="notes">Notes</label>
        <textarea id="notes" bind:value={consumptionData.notes} rows="3" placeholder="Additional notes..."></textarea>
      </div>
    </div>
    
    <button type="submit" disabled={isLogging}>
      {#if isLogging}
        Logging...
      {:else}
        Log Consumption
      {/if}
    </button>
  </form>
</div>

<style>
  .consumption-logger {
    background: var(--card-bg);
    padding: 1rem;
    border-radius: 8px;
  }
  
  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
    margin-bottom: 1rem;
  }
  
  .full-width {
    grid-column: 1 / -1;
  }
</style>

Batch Tracking and Best-Before Dates

Monitor beer freshness with batch tracking and expiration alerts.

Expiry Monitoring

Svelte
<!-- src/components/collection/ExpiryMonitor.svelte -->
<script>
  let { beers } = $props();
  
  // Filter beers with best-before dates
  let beersWithExpiry = $derived(() => {
    return beers
      .filter(beer => beer.bestBeforeDate)
      .sort((a, b) => new Date(a.bestBeforeDate) - new Date(b.bestBeforeDate));
  });
  
  // Categorize by expiry status
  let expiryCategories = $derived(() => {
    const now = new Date();
    const categories = {
      expired: [],
      expiringSoon: [], // within 30 days
      good: []
    };
    
    for (const beer of beersWithExpiry) {
      const expiryDate = new Date(beer.bestBeforeDate);
      const daysUntilExpiry = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24));
      
      if (daysUntilExpiry < 0) {
        categories.expired.push({ ...beer, daysUntilExpiry });
      } else if (daysUntilExpiry <= 30) {
        categories.expiringSoon.push({ ...beer, daysUntilExpiry });
      } else {
        categories.good.push({ ...beer, daysUntilExpiry });
      }
    }
    
    return categories;
  });
</script>

<div class="expiry-monitor">
  {#each Object.entries(expiryCategories) as [category, beers]}
    {#if beers.length > 0}
      <div class="expiry-section {category}">
        <h4>
          {#if category === 'expired'}
            Expired Beers ({beers.length})
          {:else if category === 'expiringSoon'}
            Expiring Soon ({beers.length})
          {:else}
            Good to Drink ({beers.length})
          {/if}
        </h4>
        
        <div class="beer-list">
          {#each beers as beer}
            <div class="beer-item">
              <div class="beer-info">
                <strong>{beer.name}</strong> by {beer.brewery}
                {#if beer.batchNumber}
                  <br><small>Batch: {beer.batchNumber}</small>
                {/if}
              </div>
              <div class="expiry-info">
                {#if beer.daysUntilExpiry < 0}
                  Expired {Math.abs(beer.daysUntilExpiry)} days ago
                {:else}
                  {beer.daysUntilExpiry} days left
                {/if}
              </div>
            </div>
          {/each}
        </div>
      </div>
    {/if}
  {/each}
</div>

<style>
  .expiry-monitor {
    space-y: 1rem;
  }
  
  .expiry-section {
    background: var(--card-bg);
    padding: 1rem;
    border-radius: 8px;
    margin-bottom: 1rem;
  }
  
  .expired {
    border-left: 4px solid var(--error);
  }
  
  .expiringSoon {
    border-left: 4px solid var(--warning);
  }
  
  .good {
    border-left: 4px solid var(--success);
  }
  
  .beer-list {
    space-y: 0.5rem;
  }
  
  .beer-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.5rem;
    background: var(--bg-secondary);
    border-radius: 4px;
  }
</style>

Next Steps