feat: opportunity detail overhaul, catalog improvements, cleanup
- api: remove stray console.log debug lines across fetch routes, controllers, and workflow dispatch - api: refactor OpportunityController and cwProbabilityCache - api: add workflow history endpoint updates - ui: overhaul opportunity detail page styles and layout - ui: remove DiscountsTab (consolidated elsewhere), update ProductsTab - ui: improve catalog page with inventory popover - ui: update sales API module and server-side page loader - ui: add sleepy-cat asset, simplify NoResultsMonkey component
This commit is contained in:
@@ -20,8 +20,6 @@ export default createRoute(
|
||||
const includeAllContacts = c.req.query("includeAllContacts") === "true";
|
||||
const includeAllAddresses = c.req.query("includeAllAddresses") === "true";
|
||||
|
||||
console.log(company.toJson({ includeAddress, includePrimaryContact, includeAllContacts }));
|
||||
|
||||
// Check for address-specific permission if includeAddress is requested
|
||||
if (includeAddress) {
|
||||
const user = c.get("user");
|
||||
|
||||
@@ -35,8 +35,6 @@ export default createRoute(
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
console.log("Creating Credential Type with data:", data);
|
||||
|
||||
const credentialType = await credentialTypes.create(data as any);
|
||||
|
||||
const response = apiResponse.created(
|
||||
|
||||
@@ -36,7 +36,10 @@ export default createRoute(
|
||||
if (includes.has("products")) {
|
||||
subResourcePromises.products = item
|
||||
.fetchProducts()
|
||||
.then((products) => products.map((p) => p.toJson()));
|
||||
.then((products) => {
|
||||
const json = products.map((p) => p.toJson());
|
||||
return json;
|
||||
});
|
||||
}
|
||||
if (includes.has("quotes")) {
|
||||
subResourcePromises.quotes = generatedQuotes
|
||||
|
||||
@@ -39,7 +39,11 @@ export default createRoute(
|
||||
if (includes.has("products")) {
|
||||
subResourcePromises.products = item
|
||||
.fetchProducts()
|
||||
.then((products) => products.map((p) => p.toJson()));
|
||||
.then((products) => {
|
||||
const json = products.map((p) => p.toJson());
|
||||
console.log(`[PRODUCTS_DEBUG] cwOpportunityId=${item.cwOpportunityId} count=${json.length}`, JSON.stringify(json, null, 2));
|
||||
return json;
|
||||
});
|
||||
}
|
||||
if (includes.has("quotes")) {
|
||||
const includeRegenData = c.req.query("includeRegenData") === "true";
|
||||
|
||||
@@ -116,15 +116,7 @@ export default createRoute(
|
||||
try {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
console.log(
|
||||
"[Workflow Dispatch] Raw request body:",
|
||||
JSON.stringify(body, null, 2),
|
||||
);
|
||||
const parsed = dispatchSchema.parse(body);
|
||||
console.log(
|
||||
"[Workflow Dispatch] Parsed payload:",
|
||||
JSON.stringify(parsed.payload, null, 2),
|
||||
);
|
||||
const user = c.get("user");
|
||||
|
||||
// ── Resolve opportunity ────────────────────────────────────────────
|
||||
|
||||
@@ -222,8 +222,6 @@ export class OpportunityController {
|
||||
activities?: ActivityController[];
|
||||
}
|
||||
) {
|
||||
console.log(data.primarySalesRep);
|
||||
|
||||
// New schema: uid is the internal PK (string), id is the CW opportunity ID (Int)
|
||||
this.id = data.uid;
|
||||
this.cwOpportunityId = data.id;
|
||||
@@ -827,6 +825,8 @@ export class OpportunityController {
|
||||
},
|
||||
});
|
||||
|
||||
console.log("[ROWS_DEBUG]", rows)
|
||||
|
||||
let ordered = rows;
|
||||
if (this.productSequence.length > 0) {
|
||||
const byId = new Map(rows.map((row) => [row.id, row]));
|
||||
@@ -1068,8 +1068,6 @@ export class OpportunityController {
|
||||
quoNarrativeProduct?.customerDescription ??
|
||||
undefined;
|
||||
|
||||
console.log("[generateQuote] quoteNarrative:", quoteNarrative);
|
||||
|
||||
const companyLine = this.companyName ?? company?.name ?? "Customer Company";
|
||||
|
||||
// Only show attention if it differs from the customer name
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
export let onHand: number | undefined;
|
||||
|
||||
// Warehouse IDs for the fixed locations
|
||||
const MURRAY_ID = 1;
|
||||
// Murray = Andrus 301 (1) + Andrus-205C (26) + Andrus 205D (27)
|
||||
const MURRAY_IDS = new Set([1, 26, 27]);
|
||||
const UNION_CITY_ID = 6;
|
||||
const LONDON_ID = 30;
|
||||
|
||||
@@ -56,7 +57,7 @@
|
||||
for (const row of rows) {
|
||||
const wId = row.warehouseId;
|
||||
const wName = row.warehouse?.name ?? "";
|
||||
if (wId === MURRAY_ID) {
|
||||
if (wId != null && MURRAY_IDS.has(wId)) {
|
||||
result.murray += row.qtyOnHand;
|
||||
} else if (wId === UNION_CITY_ID) {
|
||||
result.unionCity += row.qtyOnHand;
|
||||
|
||||
@@ -3,64 +3,15 @@
|
||||
export let size: number = 160;
|
||||
</script>
|
||||
|
||||
<div class="monkey" style="width: {size}px">
|
||||
<svg viewBox="0 0 120 120" width="100%" height="100%" aria-hidden="true">
|
||||
<!-- head -->
|
||||
<circle cx="60" cy="60" r="44" fill="#8B5E3C" />
|
||||
<!-- face -->
|
||||
<ellipse cx="60" cy="70" rx="30" ry="24" fill="#E8C9A1" />
|
||||
<!-- ears -->
|
||||
<circle cx="26" cy="56" r="12" fill="#8B5E3C" />
|
||||
<circle cx="94" cy="56" r="12" fill="#8B5E3C" />
|
||||
<circle cx="26" cy="56" r="6" fill="#E8C9A1" />
|
||||
<circle cx="94" cy="56" r="6" fill="#E8C9A1" />
|
||||
<!-- eyes -->
|
||||
<circle cx="48" cy="64" r="6" fill="#2b2b2b" />
|
||||
<circle cx="72" cy="64" r="6" fill="#2b2b2b" />
|
||||
<!-- smile -->
|
||||
<path
|
||||
d="M48 78 Q60 88 72 78"
|
||||
stroke="#2b2b2b"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<!-- eyebrow accents -->
|
||||
<path
|
||||
d="M42 58 Q48 54 54 58"
|
||||
stroke="#6b4a33"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<path
|
||||
d="M66 58 Q72 54 78 58"
|
||||
stroke="#6b4a33"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<!-- tiny tuft -->
|
||||
<path
|
||||
d="M60 28 Q58 34 62 36"
|
||||
stroke="#6b4a33"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div class="msg">{message}</div>
|
||||
<div class="empty-state">
|
||||
<p class="msg">{message}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.monkey {
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
.msg {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "@testing-library/svelte";
|
||||
import NoResultsMonkey from "./NoResultsMonkey.svelte";
|
||||
|
||||
describe("NoResultsMonkey", () => {
|
||||
it("renders with default message", () => {
|
||||
render(NoResultsMonkey);
|
||||
|
||||
expect(screen.getByText("No results found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with custom message", () => {
|
||||
render(NoResultsMonkey, { props: { message: "Nothing here" } });
|
||||
|
||||
expect(screen.getByText("Nothing here")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders SVG illustration", () => {
|
||||
const { container } = render(NoResultsMonkey);
|
||||
|
||||
const svg = container.querySelector("svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies custom size", () => {
|
||||
const { container } = render(NoResultsMonkey, {
|
||||
props: { size: 200 },
|
||||
});
|
||||
|
||||
const wrapper = container.querySelector(".monkey");
|
||||
expect(wrapper).toHaveStyle("width: 200px");
|
||||
});
|
||||
});
|
||||
@@ -654,6 +654,9 @@ export const sales = {
|
||||
},
|
||||
|
||||
async fetchProducts(accessToken: string, identifier: string) {
|
||||
|
||||
console.log("fetch prod exec check")
|
||||
|
||||
const response = await api.get(
|
||||
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
|
||||
identifier
|
||||
@@ -664,6 +667,9 @@ export const sales = {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[fetchProducts] API response:", JSON.stringify(response.data, null, 2));
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import NoResultsMonkey from "../../../components/NoResultsMonkey.svelte";
|
||||
import AccessDenied from "../../../components/AccessDenied.svelte";
|
||||
import Pagination from "../../../components/Pagination.svelte";
|
||||
import InventoryPopover from "../../../components/InventoryPopover.svelte";
|
||||
import { formatDate } from "$lib/utils";
|
||||
import "../../../styles/procurement/catalog.css";
|
||||
import { clientFetch } from "$lib/client-fetch";
|
||||
@@ -599,7 +598,16 @@
|
||||
<td class="col-price">{formatCurrency(item.price)}</td>
|
||||
<td class="col-cost">{formatCurrency(item.cost)}</td>
|
||||
<td class="col-onhand">
|
||||
<InventoryPopover identifier={item.id} onHand={item.onHand} />
|
||||
<span
|
||||
class="onhand-badge"
|
||||
class:onhand-zero={item.onHand === 0}
|
||||
class:onhand-low={item.onHand != null &&
|
||||
item.onHand > 0 &&
|
||||
item.onHand <= 3}
|
||||
class:onhand-ok={item.onHand != null && item.onHand > 3}
|
||||
>
|
||||
{item.onHand ?? "—"}
|
||||
</span>
|
||||
</td>
|
||||
<td class="col-status">
|
||||
<span
|
||||
@@ -713,7 +721,17 @@
|
||||
<div class="detail-field">
|
||||
<span class="detail-label">On Hand</span>
|
||||
<span class="detail-value">
|
||||
<InventoryPopover identifier={selectedItem.id} onHand={selectedItem.onHand} />
|
||||
<span
|
||||
class="onhand-badge"
|
||||
class:onhand-zero={selectedItem.onHand === 0}
|
||||
class:onhand-low={selectedItem.onHand != null &&
|
||||
selectedItem.onHand > 0 &&
|
||||
selectedItem.onHand <= 3}
|
||||
class:onhand-ok={selectedItem.onHand != null &&
|
||||
selectedItem.onHand > 3}
|
||||
>
|
||||
{selectedItem.onHand ?? "—"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -948,7 +966,18 @@
|
||||
<div class="linked-detail-field">
|
||||
<span class="detail-label">On Hand</span>
|
||||
<span class="detail-value">
|
||||
<InventoryPopover identifier={li.id} onHand={li.onHand} />
|
||||
<span
|
||||
class="onhand-badge"
|
||||
class:onhand-zero={li.onHand === 0}
|
||||
class:onhand-low={li.onHand != null &&
|
||||
li.onHand > 0 &&
|
||||
li.onHand <= 3}
|
||||
class:onhand-ok={li.onHand != null &&
|
||||
li.onHand > 3}
|
||||
>
|
||||
{li.onHand ?? "—"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="linked-detail-field">
|
||||
<span class="detail-label">Status</span>
|
||||
@@ -1303,7 +1332,17 @@
|
||||
<div class="link-preview-field">
|
||||
<span class="detail-label">On Hand</span>
|
||||
<span class="detail-value">
|
||||
<InventoryPopover identifier={linkPreviewItem.id} onHand={linkPreviewItem.onHand} />
|
||||
<span
|
||||
class="onhand-badge"
|
||||
class:onhand-zero={linkPreviewItem.onHand === 0}
|
||||
class:onhand-low={linkPreviewItem.onHand != null &&
|
||||
linkPreviewItem.onHand > 0 &&
|
||||
linkPreviewItem.onHand <= 3}
|
||||
class:onhand-ok={linkPreviewItem.onHand != null &&
|
||||
linkPreviewItem.onHand > 3}
|
||||
>
|
||||
{linkPreviewItem.onHand ?? "—"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{#if linkPreviewItem.manufacturer}
|
||||
|
||||
@@ -68,6 +68,8 @@ export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
|
||||
console.log("[page.server] Full API response:", JSON.stringify(result, null, 2));
|
||||
|
||||
const opportunity = result?.data ?? null;
|
||||
const notes = result?.data?.notes ?? [];
|
||||
const contacts = result?.data?.contacts ?? [];
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
$: notes = data.notes;
|
||||
$: contacts = data.contacts;
|
||||
$: products = data.products;
|
||||
$: console.log("[OpportunityPage] full API response data:", data);
|
||||
$: quotes = data.quotes ?? [];
|
||||
$: permissions = data.permissions;
|
||||
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { OpportunityProduct } from "../types";
|
||||
import NoResultsMonkey from "../../../../../components/NoResultsMonkey.svelte";
|
||||
|
||||
export let products: OpportunityProduct[] = [];
|
||||
|
||||
$: discountedProducts = products.filter(
|
||||
(p) => (p.discount ?? 0) !== 0 && !p.cancelled
|
||||
);
|
||||
|
||||
$: totalDiscount = discountedProducts.reduce(
|
||||
(sum, p) => sum + (p.discount ?? 0) * (p.quantity ?? 1),
|
||||
0
|
||||
);
|
||||
|
||||
$: totalListPrice = discountedProducts.reduce(
|
||||
(sum, p) => sum + (p.listPrice ?? 0) * (p.quantity ?? 1),
|
||||
0
|
||||
);
|
||||
|
||||
$: totalUnitPrice = discountedProducts.reduce(
|
||||
(sum, p) => sum + (p.unitPrice ?? 0) * (p.quantity ?? 1),
|
||||
0
|
||||
);
|
||||
|
||||
function fmtMoney(value: number): string {
|
||||
return value.toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
}
|
||||
|
||||
function fmtPercent(list: number, discount: number): string {
|
||||
if (list === 0) return "—";
|
||||
const pct = (discount / list) * 100;
|
||||
return `${pct.toFixed(1)}%`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="discounts-tab">
|
||||
<!-- Summary cards -->
|
||||
{#if discountedProducts.length > 0}
|
||||
<div class="discounts-summary">
|
||||
<div class="discount-summary-card">
|
||||
<span class="discount-summary-label">Total List Price</span>
|
||||
<span class="discount-summary-value">{fmtMoney(totalListPrice)}</span>
|
||||
</div>
|
||||
<div class="discount-summary-card">
|
||||
<span class="discount-summary-label">Total Discounts</span>
|
||||
<span class="discount-summary-value discount-negative"
|
||||
>{fmtMoney(totalDiscount)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="discount-summary-card">
|
||||
<span class="discount-summary-label">Net Price</span>
|
||||
<span class="discount-summary-value">{fmtMoney(totalUnitPrice)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Discounts table -->
|
||||
{#if discountedProducts.length === 0}
|
||||
<div class="tab-empty">
|
||||
<NoResultsMonkey message="No discounts applied" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="discounts-table-wrap">
|
||||
<table class="discounts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-product">Product</th>
|
||||
<th class="col-num">Qty</th>
|
||||
<th class="col-num">List Price</th>
|
||||
<th class="col-num">Discount</th>
|
||||
<th class="col-num">% Off</th>
|
||||
<th class="col-num">Unit Price</th>
|
||||
<th class="col-num">Ext. Discount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each discountedProducts as product (product.id)}
|
||||
<tr>
|
||||
<td class="col-product">
|
||||
<div class="discount-product-name">
|
||||
{product.forecastDescription || product.productDescription || "—"}
|
||||
</div>
|
||||
{#if product.catalogItem?.identifier}
|
||||
<div class="discount-product-sku">
|
||||
{product.catalogItem.identifier}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="col-num">{product.quantity ?? 0}</td>
|
||||
<td class="col-num">{fmtMoney(product.listPrice ?? 0)}</td>
|
||||
<td class="col-num discount-negative">
|
||||
{fmtMoney(product.discount ?? 0)}
|
||||
</td>
|
||||
<td class="col-num">
|
||||
{fmtPercent(product.listPrice ?? 0, product.discount ?? 0)}
|
||||
</td>
|
||||
<td class="col-num">{fmtMoney(product.unitPrice ?? 0)}</td>
|
||||
<td class="col-num discount-negative">
|
||||
{fmtMoney((product.discount ?? 0) * (product.quantity ?? 1))}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="col-product"><strong>Total</strong></td>
|
||||
<td class="col-num"></td>
|
||||
<td class="col-num"><strong>{fmtMoney(totalListPrice)}</strong></td>
|
||||
<td class="col-num discount-negative">
|
||||
<strong>{fmtMoney(totalDiscount)}</strong>
|
||||
</td>
|
||||
<td class="col-num">
|
||||
{fmtPercent(totalListPrice, totalDiscount)}
|
||||
</td>
|
||||
<td class="col-num"><strong>{fmtMoney(totalUnitPrice)}</strong></td>
|
||||
<td class="col-num discount-negative">
|
||||
<strong>{fmtMoney(totalDiscount)}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.discounts-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.discounts-summary {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.discount-summary-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-raised, #f7f8fa);
|
||||
border: 1px solid var(--border-subtle, #e2e5ea);
|
||||
}
|
||||
|
||||
.discount-summary-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #6b7280);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.discount-summary-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a2e);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.discount-negative {
|
||||
color: var(--color-danger, #dc2626);
|
||||
}
|
||||
|
||||
.discounts-table-wrap {
|
||||
overflow-x: auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.discounts-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.discounts-table th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--text-muted, #6b7280);
|
||||
border-bottom: 2px solid var(--border-subtle, #e2e5ea);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.discounts-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle, #e2e5ea);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.discounts-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.discounts-table tbody tr:hover {
|
||||
background: var(--bg-hover, rgba(0, 0, 0, 0.02));
|
||||
}
|
||||
|
||||
.discounts-table tfoot td {
|
||||
padding: 10px 12px;
|
||||
border-top: 2px solid var(--border-subtle, #e2e5ea);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.col-product {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.col-num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.discounts-table th.col-num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.discount-product-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1a1a2e);
|
||||
}
|
||||
|
||||
.discount-product-sku {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted, #6b7280);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.discounts-summary {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import { optima } from "$lib";
|
||||
import NoResultsMonkey from "../../../../../components/NoResultsMonkey.svelte";
|
||||
import AddProductModal from "../../../../../components/AddProductModal.svelte";
|
||||
import InventoryPopover from "../../../../../components/InventoryPopover.svelte";
|
||||
import type { CatalogItem } from "$lib/optima-api/modules/procurement";
|
||||
import type {
|
||||
AddProductBody,
|
||||
@@ -2557,12 +2556,7 @@
|
||||
<div class="detail-field">
|
||||
<span class="detail-field-label">On Hand</span>
|
||||
<span class="detail-field-value">
|
||||
{#if selectedProduct.catalogItem}
|
||||
<InventoryPopover
|
||||
identifier={String(selectedProduct.catalogItem.id)}
|
||||
onHand={selectedProduct.onHand ?? undefined}
|
||||
/>
|
||||
{:else if selectedProduct.onHand != null}
|
||||
{#if selectedProduct.onHand != null}
|
||||
<span
|
||||
class="stock-badge"
|
||||
class:stock-none={selectedProduct.onHand === 0}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Reference in New Issue
Block a user