This commit is contained in:
@@ -41,14 +41,16 @@ jobs:
|
||||
# Reset any local changes (e.g., package-lock.json, build artifacts) and sync to origin/main
|
||||
git fetch --prune origin
|
||||
git reset --hard origin/main
|
||||
git clean -fdx
|
||||
git clean -fdx -e .env -e .env.*
|
||||
# Install Node.js deps and build without modifying lockfile
|
||||
"$NPM" ci --no-audit --no-fund
|
||||
"$NPM" run build
|
||||
# Ensure systemd service exists and restart
|
||||
sudo -n systemctl restart train-id
|
||||
if systemctl list-unit-files | grep -q "${SERVICE}.service"; then
|
||||
sudo systemctl restart "$SERVICE"
|
||||
else
|
||||
sudo systemctl start "$SERVICE"
|
||||
echo "Warning: ${SERVICE}.service not found; start your process manager manually."
|
||||
fi
|
||||
EOF
|
||||
|
||||
@@ -77,11 +77,17 @@
|
||||
|
||||
<section id="tab-prices" class="card" style="display:none;">
|
||||
<h3>Price Report</h3>
|
||||
<div style="margin-bottom:16px; font-size:1.2em; font-weight:bold;">
|
||||
Total Value: <span id="totalPrice">$0.00</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="updatePrices">Update Prices</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap;">
|
||||
<canvas id="pricePie" width="360" height="360" style="border:1px solid #eee; border-radius:8px;"></canvas>
|
||||
<div style="position:relative;">
|
||||
<canvas id="pricePie" width="360" height="360" style="border:1px solid #eee; border-radius:8px; cursor:pointer;"></canvas>
|
||||
<div id="pieTooltip" style="position:absolute; display:none; background:rgba(0,0,0,0.8); color:white; padding:8px; border-radius:4px; pointer-events:none; white-space:nowrap; z-index:1000;"></div>
|
||||
</div>
|
||||
<div style="flex:1; min-width:280px;">
|
||||
<table id="skuPrices">
|
||||
<thead>
|
||||
@@ -115,6 +121,9 @@
|
||||
const skuTbody = document.querySelector('#skuPrices tbody');
|
||||
const updatePricesBtn = $('#updatePrices');
|
||||
const pricePie = /** @type {HTMLCanvasElement} */ (document.getElementById('pricePie'));
|
||||
const totalPriceEl = $('#totalPrice');
|
||||
const pieTooltip = $('#pieTooltip');
|
||||
let pieSliceData = []; // Store slice data for hover detection
|
||||
|
||||
function showTab(tab) {
|
||||
if (tab === 'items') {
|
||||
@@ -172,6 +181,7 @@
|
||||
const ctx = pricePie.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, pricePie.width, pricePie.height);
|
||||
pieSliceData = [];
|
||||
const total = items.reduce((sum, it) => sum + (it.totalValue || 0), 0);
|
||||
if (total <= 0) {
|
||||
ctx.fillStyle = '#666';
|
||||
@@ -186,6 +196,15 @@
|
||||
const angle = (val / total) * Math.PI * 2;
|
||||
const end = start + angle;
|
||||
const hue = (i * 47) % 360;
|
||||
|
||||
// Store slice data for hover detection
|
||||
pieSliceData.push({
|
||||
start,
|
||||
end,
|
||||
item: it,
|
||||
hue
|
||||
});
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r, start, end);
|
||||
@@ -195,11 +214,77 @@
|
||||
start = end;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle pie chart hover
|
||||
pricePie.addEventListener('mousemove', (e) => {
|
||||
const rect = pricePie.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left - pricePie.width / 2;
|
||||
const y = e.clientY - rect.top - pricePie.height / 2;
|
||||
const distance = Math.sqrt(x * x + y * y);
|
||||
const cx = pricePie.width / 2;
|
||||
const cy = pricePie.height / 2;
|
||||
const r = Math.min(cx, cy) - 8;
|
||||
|
||||
if (distance > r) {
|
||||
pieTooltip.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate angle from center (atan2 gives angle from positive x-axis, clockwise)
|
||||
let angle = Math.atan2(y, x);
|
||||
// Normalize to 0-2π and adjust for pie starting at top (-Math.PI/2)
|
||||
// Add Math.PI/2 to rotate so top is 0, then normalize to 0-2π
|
||||
angle = angle + Math.PI / 2;
|
||||
if (angle < 0) angle += 2 * Math.PI;
|
||||
if (angle >= 2 * Math.PI) angle -= 2 * Math.PI;
|
||||
|
||||
// Find which slice contains this angle
|
||||
for (const slice of pieSliceData) {
|
||||
// Slices start at -Math.PI/2 (top), normalize to 0-2π
|
||||
let sliceStart = slice.start + Math.PI / 2;
|
||||
let sliceEnd = slice.end + Math.PI / 2;
|
||||
|
||||
// Normalize to 0-2π
|
||||
if (sliceStart < 0) sliceStart += 2 * Math.PI;
|
||||
if (sliceEnd < 0) sliceEnd += 2 * Math.PI;
|
||||
if (sliceStart >= 2 * Math.PI) sliceStart -= 2 * Math.PI;
|
||||
if (sliceEnd >= 2 * Math.PI) sliceEnd -= 2 * Math.PI;
|
||||
|
||||
let contains = false;
|
||||
if (sliceStart <= sliceEnd) {
|
||||
// Normal case: slice doesn't wrap around
|
||||
contains = angle >= sliceStart && angle <= sliceEnd;
|
||||
} else {
|
||||
// Slice wraps around 0 (e.g., starts at 350° and ends at 10°)
|
||||
contains = angle >= sliceStart || angle <= sliceEnd;
|
||||
}
|
||||
|
||||
if (contains) {
|
||||
const it = slice.item;
|
||||
pieTooltip.textContent = `ID: ${it.id || 'N/A'} | ${it.description || 'No name'} | $${it.totalValue.toFixed(2)}`;
|
||||
pieTooltip.style.display = 'block';
|
||||
pieTooltip.style.left = (e.clientX - rect.left + 10) + 'px';
|
||||
pieTooltip.style.top = (e.clientY - rect.top + 10) + 'px';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pieTooltip.style.display = 'none';
|
||||
});
|
||||
|
||||
pricePie.addEventListener('mouseleave', () => {
|
||||
pieTooltip.style.display = 'none';
|
||||
});
|
||||
|
||||
async function loadPriceReport() {
|
||||
const res = await fetch('/api/price-report');
|
||||
const data = await res.json();
|
||||
// table of skus
|
||||
|
||||
// Calculate and display total price
|
||||
const total = data.items.reduce((sum, it) => sum + (it.totalValue || 0), 0);
|
||||
totalPriceEl.textContent = `$${total.toFixed(2)}`;
|
||||
|
||||
// table of skus (already sorted by ID from API)
|
||||
skuTbody.innerHTML = '';
|
||||
for (const s of data.skus) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
@@ -277,7 +277,13 @@ router.get('/price-report', async (_req, res) => {
|
||||
};
|
||||
}
|
||||
}
|
||||
const skuList = Object.values(skuListMap).sort((a, b) => (a.sku || '').localeCompare(b.sku || ''));
|
||||
const skuList = Object.values(skuListMap).sort((a, b) => {
|
||||
// Sort by ID first (null IDs go to end), then by SKU
|
||||
if (a.id === null && b.id === null) return (a.sku || '').localeCompare(b.sku || '');
|
||||
if (a.id === null) return 1;
|
||||
if (b.id === null) return -1;
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
});
|
||||
res.json({ items: itemValues, skus: skuList });
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user