feat: AI price estimation for shopping list with per-item real-time display
- Add get_shopping_price / get_all_shopping_prices API endpoints - AI (Gemini) estimates retail price per natural unit (pack, piece, bunch) instead of always per-kg — avoids absurd totals like €1609 - _calcEstimatedTotal: proper g/ml→package conversion using defQty + regex on unit_label; only 'kg'/'l' labels trigger weight/volume math - Cache key bumped to v2 to invalidate old per-kg cached entries - Suggested quantity cap lowered from 20 to 10 conf/pz - Unit mismatch guard: if totalUsed >> buyCount*5 for unit=conf, use purchase frequency instead of raw consumption rate - JS _buildPricePayload: use smartShoppingItems for qty/unit (not Bring! spec) - JS _cachedPrices: persist in sessionStorage (survives navigation); validated by _qty/_unit metadata so stale totals auto-invalidate - Price display redesigned: right-side column per row (price-col-main + price-col-unit) instead of small inline badge - fetchAllPrices: buttons disabled immediately before guard check; running total uses only current shoppingItems (not Object.values cache) - Background refresh: always silent (removed 90s interaction condition) - visibilitychange: sets _bgCall=true for shopping before refreshCurrentPage - .gitignore: add runtime data files (bring_migrate_ts, shopping_price_cache, anomaly_dismissed, opened_shelf_cache, shopping_name_cache) - Remove bring_catalog.json and bring_migrate_ts.json from tracking
This commit is contained in:
@@ -516,6 +516,24 @@ body {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Skeleton shimmer while stat card data is loading */
|
||||
@keyframes stat-shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.stat-value.stat-loading {
|
||||
color: transparent !important;
|
||||
background: linear-gradient(90deg, var(--border) 25%, color-mix(in srgb, var(--border) 40%, white) 50%, var(--border) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: stat-shimmer 1.2s ease-in-out infinite;
|
||||
border-radius: 8px;
|
||||
min-width: 40px;
|
||||
min-height: 2.4rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
@@ -1861,6 +1879,121 @@ body {
|
||||
.badge-local-tag { background: #e0f2fe; color: #0369a1; cursor: pointer; }
|
||||
.badge-local-tag:hover { background: #bae6fd; }
|
||||
|
||||
/* ─── Shopping price badge ─── */
|
||||
.shopping-item-price-badge {
|
||||
margin-top: 4px;
|
||||
min-height: 1.2rem;
|
||||
}
|
||||
.price-badge-value {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #15803d;
|
||||
background: #dcfce7;
|
||||
border-radius: 8px;
|
||||
padding: 1px 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.price-badge-loading {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
}
|
||||
.price-badge-error {
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ─── Price column — right-side per-item price display ─── */
|
||||
.shopping-item-price-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 58px;
|
||||
text-align: right;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.price-col-main {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #15803d;
|
||||
white-space: nowrap;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.price-col-unit {
|
||||
font-size: 0.62rem;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.price-col-loading {
|
||||
font-size: 0.7rem;
|
||||
color: #cbd5e1;
|
||||
font-style: italic;
|
||||
}
|
||||
.price-col-error {
|
||||
font-size: 0.75rem;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* ─── Price summary bar (top of shopping tab) ─── */
|
||||
.shopping-price-total-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 4px;
|
||||
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
|
||||
border-radius: 10px;
|
||||
margin: 8px 0 4px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: #166534;
|
||||
box-shadow: 0 1px 4px rgba(21,128,61,0.08);
|
||||
}
|
||||
.price-total-label {
|
||||
flex: 1;
|
||||
}
|
||||
.price-total-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-price-refresh {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-price-refresh:hover { background: rgba(0,0,0,0.07); }
|
||||
.btn-price-refresh:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
/* Loading progress bar */
|
||||
.price-loading-bar {
|
||||
height: 3px;
|
||||
background: #dcfce7;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 0 12px 6px;
|
||||
}
|
||||
.price-loading-inner {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: #16a34a;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
@keyframes price-sweep {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(350%); }
|
||||
}
|
||||
|
||||
/* Tag add button */
|
||||
.shopping-item-tag-btn {
|
||||
background: none;
|
||||
|
||||
Reference in New Issue
Block a user