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
|
# Reset any local changes (e.g., package-lock.json, build artifacts) and sync to origin/main
|
||||||
git fetch --prune origin
|
git fetch --prune origin
|
||||||
git reset --hard origin/main
|
git reset --hard origin/main
|
||||||
git clean -fdx
|
git clean -fdx -e .env -e .env.*
|
||||||
# Install Node.js deps and build without modifying lockfile
|
# Install Node.js deps and build without modifying lockfile
|
||||||
"$NPM" ci --no-audit --no-fund
|
"$NPM" ci --no-audit --no-fund
|
||||||
"$NPM" run build
|
"$NPM" run build
|
||||||
# Ensure systemd service exists and restart
|
# Ensure systemd service exists and restart
|
||||||
|
sudo -n systemctl restart train-id
|
||||||
if systemctl list-unit-files | grep -q "${SERVICE}.service"; then
|
if systemctl list-unit-files | grep -q "${SERVICE}.service"; then
|
||||||
sudo systemctl restart "$SERVICE"
|
sudo systemctl restart "$SERVICE"
|
||||||
else
|
else
|
||||||
|
sudo systemctl start "$SERVICE"
|
||||||
echo "Warning: ${SERVICE}.service not found; start your process manager manually."
|
echo "Warning: ${SERVICE}.service not found; start your process manager manually."
|
||||||
fi
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -77,11 +77,17 @@
|
|||||||
|
|
||||||
<section id="tab-prices" class="card" style="display:none;">
|
<section id="tab-prices" class="card" style="display:none;">
|
||||||
<h3>Price Report</h3>
|
<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">
|
<div class="actions">
|
||||||
<button id="updatePrices">Update Prices</button>
|
<button id="updatePrices">Update Prices</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap;">
|
<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;">
|
<div style="flex:1; min-width:280px;">
|
||||||
<table id="skuPrices">
|
<table id="skuPrices">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -115,6 +121,9 @@
|
|||||||
const skuTbody = document.querySelector('#skuPrices tbody');
|
const skuTbody = document.querySelector('#skuPrices tbody');
|
||||||
const updatePricesBtn = $('#updatePrices');
|
const updatePricesBtn = $('#updatePrices');
|
||||||
const pricePie = /** @type {HTMLCanvasElement} */ (document.getElementById('pricePie'));
|
const pricePie = /** @type {HTMLCanvasElement} */ (document.getElementById('pricePie'));
|
||||||
|
const totalPriceEl = $('#totalPrice');
|
||||||
|
const pieTooltip = $('#pieTooltip');
|
||||||
|
let pieSliceData = []; // Store slice data for hover detection
|
||||||
|
|
||||||
function showTab(tab) {
|
function showTab(tab) {
|
||||||
if (tab === 'items') {
|
if (tab === 'items') {
|
||||||
@@ -172,6 +181,7 @@
|
|||||||
const ctx = pricePie.getContext('2d');
|
const ctx = pricePie.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
ctx.clearRect(0, 0, pricePie.width, pricePie.height);
|
ctx.clearRect(0, 0, pricePie.width, pricePie.height);
|
||||||
|
pieSliceData = [];
|
||||||
const total = items.reduce((sum, it) => sum + (it.totalValue || 0), 0);
|
const total = items.reduce((sum, it) => sum + (it.totalValue || 0), 0);
|
||||||
if (total <= 0) {
|
if (total <= 0) {
|
||||||
ctx.fillStyle = '#666';
|
ctx.fillStyle = '#666';
|
||||||
@@ -186,6 +196,15 @@
|
|||||||
const angle = (val / total) * Math.PI * 2;
|
const angle = (val / total) * Math.PI * 2;
|
||||||
const end = start + angle;
|
const end = start + angle;
|
||||||
const hue = (i * 47) % 360;
|
const hue = (i * 47) % 360;
|
||||||
|
|
||||||
|
// Store slice data for hover detection
|
||||||
|
pieSliceData.push({
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
item: it,
|
||||||
|
hue
|
||||||
|
});
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(cx, cy);
|
ctx.moveTo(cx, cy);
|
||||||
ctx.arc(cx, cy, r, start, end);
|
ctx.arc(cx, cy, r, start, end);
|
||||||
@@ -195,11 +214,77 @@
|
|||||||
start = end;
|
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() {
|
async function loadPriceReport() {
|
||||||
const res = await fetch('/api/price-report');
|
const res = await fetch('/api/price-report');
|
||||||
const data = await res.json();
|
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 = '';
|
skuTbody.innerHTML = '';
|
||||||
for (const s of data.skus) {
|
for (const s of data.skus) {
|
||||||
const tr = document.createElement('tr');
|
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 });
|
res.json({ items: itemValues, skus: skuList });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
Reference in New Issue
Block a user