fix: fixed warehouse inventory numbers and some button verbage
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
<script lang="ts">
|
||||
import { clientFetch } from "$lib/client-fetch";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let identifier: string;
|
||||
export let onHand: number | undefined;
|
||||
|
||||
// Warehouse IDs for the fixed locations
|
||||
const MURRAY_ID = 1;
|
||||
const UNION_CITY_ID = 6;
|
||||
const LONDON_ID = 30;
|
||||
|
||||
type InventoryRow = {
|
||||
id: number;
|
||||
qtyOnHand: number;
|
||||
warehouseId: number | null;
|
||||
warehouseBinId: number;
|
||||
warehouse: { id: number; name: string } | null;
|
||||
};
|
||||
|
||||
type Breakdown = {
|
||||
murray: number;
|
||||
unionCity: number;
|
||||
london: number;
|
||||
vehicles: number;
|
||||
};
|
||||
|
||||
let visible = false;
|
||||
let loading = false;
|
||||
let breakdown: Breakdown | null = null;
|
||||
let fetchedFor: string | null = null;
|
||||
|
||||
let anchorEl: HTMLElement;
|
||||
let popoverX = 0;
|
||||
let popoverY = 0;
|
||||
|
||||
// Portal action — moves the node to document.body so it is never
|
||||
// clipped by a parent overflow or trapped inside a CSS transform context.
|
||||
function portal(node: HTMLElement) {
|
||||
document.body.appendChild(node);
|
||||
return {
|
||||
destroy() {
|
||||
node.parentNode?.removeChild(node);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function onhandClass(qty: number) {
|
||||
if (qty === 0) return "onhand-zero";
|
||||
if (qty <= 3) return "onhand-low";
|
||||
return "onhand-ok";
|
||||
}
|
||||
|
||||
function computeBreakdown(rows: InventoryRow[]): Breakdown {
|
||||
const result: Breakdown = { murray: 0, unionCity: 0, london: 0, vehicles: 0 };
|
||||
for (const row of rows) {
|
||||
const wId = row.warehouseId;
|
||||
const wName = row.warehouse?.name ?? "";
|
||||
if (wId === MURRAY_ID) {
|
||||
result.murray += row.qtyOnHand;
|
||||
} else if (wId === UNION_CITY_ID) {
|
||||
result.unionCity += row.qtyOnHand;
|
||||
} else if (wId === LONDON_ID) {
|
||||
result.london += row.qtyOnHand;
|
||||
} else if (/^\d+$/.test(wName)) {
|
||||
result.vehicles += row.qtyOnHand;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (fetchedFor === identifier) return;
|
||||
loading = true;
|
||||
breakdown = null;
|
||||
try {
|
||||
const result = await clientFetch<{ data: InventoryRow[] }>(
|
||||
`/procurement/catalog/inventory?id=${encodeURIComponent(identifier)}`,
|
||||
);
|
||||
breakdown = computeBreakdown(result?.data ?? []);
|
||||
fetchedFor = identifier;
|
||||
} catch {
|
||||
breakdown = { murray: 0, unionCity: 0, london: 0, vehicles: 0 };
|
||||
fetchedFor = identifier;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (!anchorEl) return;
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
popoverX = rect.left + rect.width / 2;
|
||||
popoverY = rect.top - 6;
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
updatePosition();
|
||||
visible = true;
|
||||
load();
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
visible = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="inv-popover-anchor"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
bind:this={anchorEl}
|
||||
on:mouseenter={handleMouseEnter}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
on:focusin={handleMouseEnter}
|
||||
on:focusout={handleMouseLeave}
|
||||
>
|
||||
<span
|
||||
class="onhand-badge"
|
||||
class:onhand-zero={onHand === 0}
|
||||
class:onhand-low={onHand != null && onHand > 0 && onHand <= 3}
|
||||
class:onhand-ok={onHand != null && onHand > 3}
|
||||
>
|
||||
{onHand ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if visible}
|
||||
<div
|
||||
use:portal
|
||||
class="inv-popover"
|
||||
role="tooltip"
|
||||
style="left:{popoverX}px; top:{popoverY}px;"
|
||||
>
|
||||
<div class="inv-popover-title">Inventory Breakdown</div>
|
||||
{#if loading}
|
||||
<div class="inv-popover-loading">
|
||||
<span class="inv-spinner" />
|
||||
Loading…
|
||||
</div>
|
||||
{:else if breakdown}
|
||||
<div class="inv-popover-rows">
|
||||
<div class="inv-popover-row">
|
||||
<span class="inv-location">Murray, KY</span>
|
||||
<span class="onhand-badge {onhandClass(breakdown.murray)}">{breakdown.murray}</span>
|
||||
</div>
|
||||
<div class="inv-popover-row">
|
||||
<span class="inv-location">Union City, TN</span>
|
||||
<span class="onhand-badge {onhandClass(breakdown.unionCity)}">{breakdown.unionCity}</span>
|
||||
</div>
|
||||
<div class="inv-popover-row">
|
||||
<span class="inv-location">London, KY</span>
|
||||
<span class="onhand-badge {onhandClass(breakdown.london)}">{breakdown.london}</span>
|
||||
</div>
|
||||
<div class="inv-popover-row">
|
||||
<span class="inv-location">Vehicles</span>
|
||||
<span class="onhand-badge {onhandClass(breakdown.vehicles)}">{breakdown.vehicles}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.inv-popover-anchor {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Badge styles — self-contained so component works outside catalog.css context */
|
||||
.onhand-badge {
|
||||
display: inline-block;
|
||||
min-width: 28px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onhand-zero {
|
||||
background: var(--status-inactive-bg, #fee2e2);
|
||||
color: var(--status-inactive-color, #dc2626);
|
||||
}
|
||||
|
||||
.onhand-low {
|
||||
background: var(--status-pending-bg, #fef3c7);
|
||||
color: var(--status-pending-color, #d97706);
|
||||
}
|
||||
|
||||
.onhand-ok {
|
||||
background: var(--status-active-bg, #dcfce7);
|
||||
color: var(--status-active-color, #16a34a);
|
||||
}
|
||||
|
||||
/*
|
||||
* Portalled to document.body — fixed to viewport, above everything.
|
||||
* Note: Svelte scoped styles won't apply to portalled nodes, so the
|
||||
* popover styles below use :global() to reach across the portal boundary.
|
||||
*/
|
||||
|
||||
:global(.inv-popover) {
|
||||
position: fixed;
|
||||
transform: translateX(-50%) translateY(-100%);
|
||||
z-index: 9999;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--card-hover-shadow, 0 8px 24px rgba(0, 0, 0, 0.15));
|
||||
padding: 10px 14px;
|
||||
min-width: 210px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:global(.inv-popover-title) {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:global(.inv-popover-loading) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
:global(.inv-spinner) {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--border-subtle);
|
||||
border-top-color: var(--text-secondary);
|
||||
border-radius: 50%;
|
||||
animation: inv-spin 0.6s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes inv-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
:global(.inv-popover-rows) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:global(.inv-popover-row) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
:global(.inv-location) {
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user