feat: import Seed (mycantaloupe.com) data — 1,655 assets enriched

- Added import_seed.py — reads Machine List(8).xlsx, matches by Asset ID → machine_id
- 51 new columns on assets table (route, barcode, sales, contact info, etc.)
- seed_data table with full 62-field JSON per asset for detail enrichment
- Seed data card in frontend asset detail view
- GET /api/assets/{id} now returns result.seed and result.seed_imported_at
- GPS columns from Seed ignored per request
- 1,654 matched, 199 unmatched (Seed-only assets not in MSFS)

Refs #47
This commit is contained in:
2026-05-28 23:28:08 -04:00
parent 908c67a26c
commit ae3b114ebc
3 changed files with 371 additions and 2 deletions
+66
View File
@@ -1315,6 +1315,12 @@
<div id="detailMsfsFields"></div>
</div>
<!-- Seed Data (mycantaloupe.com enrichment) -->
<div class="card" id="detailSeedCard" style="display:none;">
<div class="card-title">🌱 Cantaloupe/Seed Data</div>
<div id="detailSeedFields"></div>
</div>
<!-- Check-in History -->
<div class="card">
<div class="card-title">Check-in History <span id="checkinCount" style="color:var(--accent2);"></span></div>
@@ -3834,6 +3840,66 @@
msfsCard.style.display = 'none';
}
// Seed Data (Cantaloupe/mycantaloupe.com enrichment)
const seedCard = document.getElementById('detailSeedCard');
const seedFields = document.getElementById('detailSeedFields');
if (a.seed) {
const s = a.seed;
// Define which Seed fields to show and in what order
const seedRows = [
df('Device', s.Device, true),
df('Type', s.Type),
df('Class', s.Class),
df('Asset Family', s['Asset Family']),
df('Customer', s.Customer),
df('Management Company', s['Management Company']),
df('Branch', s.Branch),
df('Route', s.Route),
df('Subroute', s.Subroute),
df('Location', s.Location),
df('Location Code', s['Location Code']),
df('Customer Code', s['Customer Code']),
df('Place', s.Place),
df('Barcode', s.Barcode, true),
df('Serial #', s['Serial Number'], true),
df('Valid Address', s['Valid Address']),
df('Business Type', s['Business Type']),
df('Primary Consumer', s['Primary Consumer Type']),
df('Machine Branding', s['Machine Branding']),
df('Has Cashless', s['Has Cashless'] ? '✅ Yes' : '❌ No'),
df('Non-Revenue', s['Non-Revenue']),
df('Phone', s.Phone),
df('Email', s.Email),
df('Fax', s.Fax),
df('Alerts', s.Alerts),
df('Coil Alerts', s['Coil Alerts']),
df('Product Alerts', s['Product Alerts']),
df('Daily Avg Sales', s['Daily Average Sales'] != null ? '$' + parseFloat(s['Daily Average Sales']).toFixed(2) : null),
df('Monthly Sales', s['Monthly Sales'] != null ? '$' + parseFloat(s['Monthly Sales']).toFixed(2) : null),
df('Yearly Sales', s['Yearly Sales'] != null ? '$' + parseFloat(s['Yearly Sales']).toFixed(2) : null),
df('Last Restock', s['Last Restock']),
df('Days Since Restock', s['Days Since Restock']),
df('Last Contact', s['Last Contact Time']),
df('Last DEX Report', s['Last Dex Report Time']),
df('Last Inventory', s['Last Inventory']),
df('Prepick Group', s['Prepick Group']),
df('Added Date', s['Added Date']),
df('Status', s.Status),
df('Make', s.Make),
df('Model', s.Model),
df('State', s.State),
df('Postal Code', s['Postal Code']),
].filter(Boolean).join('');
if (seedRows) {
seedFields.innerHTML = seedRows;
seedCard.style.display = 'block';
} else {
seedCard.style.display = 'none';
}
} else {
seedCard.style.display = 'none';
}
await loadCheckinHistory(id);
} catch (e) {
showToast(e.message, true);