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 includeAllContacts = c.req.query("includeAllContacts") === "true";
|
||||||
const includeAllAddresses = c.req.query("includeAllAddresses") === "true";
|
const includeAllAddresses = c.req.query("includeAllAddresses") === "true";
|
||||||
|
|
||||||
console.log(company.toJson({ includeAddress, includePrimaryContact, includeAllContacts }));
|
|
||||||
|
|
||||||
// Check for address-specific permission if includeAddress is requested
|
// Check for address-specific permission if includeAddress is requested
|
||||||
if (includeAddress) {
|
if (includeAddress) {
|
||||||
const user = c.get("user");
|
const user = c.get("user");
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export default createRoute(
|
|||||||
|
|
||||||
const data = schema.parse(body);
|
const data = schema.parse(body);
|
||||||
|
|
||||||
console.log("Creating Credential Type with data:", data);
|
|
||||||
|
|
||||||
const credentialType = await credentialTypes.create(data as any);
|
const credentialType = await credentialTypes.create(data as any);
|
||||||
|
|
||||||
const response = apiResponse.created(
|
const response = apiResponse.created(
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export default createRoute(
|
|||||||
if (includes.has("products")) {
|
if (includes.has("products")) {
|
||||||
subResourcePromises.products = item
|
subResourcePromises.products = item
|
||||||
.fetchProducts()
|
.fetchProducts()
|
||||||
.then((products) => products.map((p) => p.toJson()));
|
.then((products) => {
|
||||||
|
const json = products.map((p) => p.toJson());
|
||||||
|
return json;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (includes.has("quotes")) {
|
if (includes.has("quotes")) {
|
||||||
subResourcePromises.quotes = generatedQuotes
|
subResourcePromises.quotes = generatedQuotes
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ export default createRoute(
|
|||||||
if (includes.has("products")) {
|
if (includes.has("products")) {
|
||||||
subResourcePromises.products = item
|
subResourcePromises.products = item
|
||||||
.fetchProducts()
|
.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")) {
|
if (includes.has("quotes")) {
|
||||||
const includeRegenData = c.req.query("includeRegenData") === "true";
|
const includeRegenData = c.req.query("includeRegenData") === "true";
|
||||||
|
|||||||
@@ -116,15 +116,7 @@ export default createRoute(
|
|||||||
try {
|
try {
|
||||||
const identifier = c.req.param("identifier");
|
const identifier = c.req.param("identifier");
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
console.log(
|
|
||||||
"[Workflow Dispatch] Raw request body:",
|
|
||||||
JSON.stringify(body, null, 2),
|
|
||||||
);
|
|
||||||
const parsed = dispatchSchema.parse(body);
|
const parsed = dispatchSchema.parse(body);
|
||||||
console.log(
|
|
||||||
"[Workflow Dispatch] Parsed payload:",
|
|
||||||
JSON.stringify(parsed.payload, null, 2),
|
|
||||||
);
|
|
||||||
const user = c.get("user");
|
const user = c.get("user");
|
||||||
|
|
||||||
// ── Resolve opportunity ────────────────────────────────────────────
|
// ── Resolve opportunity ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -222,8 +222,6 @@ export class OpportunityController {
|
|||||||
activities?: ActivityController[];
|
activities?: ActivityController[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
console.log(data.primarySalesRep);
|
|
||||||
|
|
||||||
// New schema: uid is the internal PK (string), id is the CW opportunity ID (Int)
|
// New schema: uid is the internal PK (string), id is the CW opportunity ID (Int)
|
||||||
this.id = data.uid;
|
this.id = data.uid;
|
||||||
this.cwOpportunityId = data.id;
|
this.cwOpportunityId = data.id;
|
||||||
@@ -827,6 +825,8 @@ export class OpportunityController {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("[ROWS_DEBUG]", rows)
|
||||||
|
|
||||||
let ordered = rows;
|
let ordered = rows;
|
||||||
if (this.productSequence.length > 0) {
|
if (this.productSequence.length > 0) {
|
||||||
const byId = new Map(rows.map((row) => [row.id, row]));
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
||||||
@@ -1068,8 +1068,6 @@ export class OpportunityController {
|
|||||||
quoNarrativeProduct?.customerDescription ??
|
quoNarrativeProduct?.customerDescription ??
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
console.log("[generateQuote] quoteNarrative:", quoteNarrative);
|
|
||||||
|
|
||||||
const companyLine = this.companyName ?? company?.name ?? "Customer Company";
|
const companyLine = this.companyName ?? company?.name ?? "Customer Company";
|
||||||
|
|
||||||
// Only show attention if it differs from the customer name
|
// Only show attention if it differs from the customer name
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
export let onHand: number | undefined;
|
export let onHand: number | undefined;
|
||||||
|
|
||||||
// Warehouse IDs for the fixed locations
|
// 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 UNION_CITY_ID = 6;
|
||||||
const LONDON_ID = 30;
|
const LONDON_ID = 30;
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const wId = row.warehouseId;
|
const wId = row.warehouseId;
|
||||||
const wName = row.warehouse?.name ?? "";
|
const wName = row.warehouse?.name ?? "";
|
||||||
if (wId === MURRAY_ID) {
|
if (wId != null && MURRAY_IDS.has(wId)) {
|
||||||
result.murray += row.qtyOnHand;
|
result.murray += row.qtyOnHand;
|
||||||
} else if (wId === UNION_CITY_ID) {
|
} else if (wId === UNION_CITY_ID) {
|
||||||
result.unionCity += row.qtyOnHand;
|
result.unionCity += row.qtyOnHand;
|
||||||
|
|||||||
@@ -3,64 +3,15 @@
|
|||||||
export let size: number = 160;
|
export let size: number = 160;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="monkey" style="width: {size}px">
|
<div class="empty-state">
|
||||||
<svg viewBox="0 0 120 120" width="100%" height="100%" aria-hidden="true">
|
<p class="msg">{message}</p>
|
||||||
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.monkey {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
|
||||||
margin: 1.5rem auto;
|
margin: 1.5rem auto;
|
||||||
}
|
}
|
||||||
.msg {
|
.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) {
|
async fetchProducts(accessToken: string, identifier: string) {
|
||||||
|
|
||||||
|
console.log("fetch prod exec check")
|
||||||
|
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
|
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
|
||||||
identifier
|
identifier
|
||||||
@@ -664,6 +667,9 @@ export const sales = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("[fetchProducts] API response:", JSON.stringify(response.data, null, 2));
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import NoResultsMonkey from "../../../components/NoResultsMonkey.svelte";
|
import NoResultsMonkey from "../../../components/NoResultsMonkey.svelte";
|
||||||
import AccessDenied from "../../../components/AccessDenied.svelte";
|
import AccessDenied from "../../../components/AccessDenied.svelte";
|
||||||
import Pagination from "../../../components/Pagination.svelte";
|
import Pagination from "../../../components/Pagination.svelte";
|
||||||
import InventoryPopover from "../../../components/InventoryPopover.svelte";
|
|
||||||
import { formatDate } from "$lib/utils";
|
import { formatDate } from "$lib/utils";
|
||||||
import "../../../styles/procurement/catalog.css";
|
import "../../../styles/procurement/catalog.css";
|
||||||
import { clientFetch } from "$lib/client-fetch";
|
import { clientFetch } from "$lib/client-fetch";
|
||||||
@@ -599,7 +598,16 @@
|
|||||||
<td class="col-price">{formatCurrency(item.price)}</td>
|
<td class="col-price">{formatCurrency(item.price)}</td>
|
||||||
<td class="col-cost">{formatCurrency(item.cost)}</td>
|
<td class="col-cost">{formatCurrency(item.cost)}</td>
|
||||||
<td class="col-onhand">
|
<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>
|
||||||
<td class="col-status">
|
<td class="col-status">
|
||||||
<span
|
<span
|
||||||
@@ -713,7 +721,17 @@
|
|||||||
<div class="detail-field">
|
<div class="detail-field">
|
||||||
<span class="detail-label">On Hand</span>
|
<span class="detail-label">On Hand</span>
|
||||||
<span class="detail-value">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -948,7 +966,18 @@
|
|||||||
<div class="linked-detail-field">
|
<div class="linked-detail-field">
|
||||||
<span class="detail-label">On Hand</span>
|
<span class="detail-label">On Hand</span>
|
||||||
<span class="detail-value">
|
<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>
|
||||||
<div class="linked-detail-field">
|
<div class="linked-detail-field">
|
||||||
<span class="detail-label">Status</span>
|
<span class="detail-label">Status</span>
|
||||||
@@ -1303,7 +1332,17 @@
|
|||||||
<div class="link-preview-field">
|
<div class="link-preview-field">
|
||||||
<span class="detail-label">On Hand</span>
|
<span class="detail-label">On Hand</span>
|
||||||
<span class="detail-value">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if linkPreviewItem.manufacturer}
|
{#if linkPreviewItem.manufacturer}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export const load: PageServerLoad = async ({ locals, params }) => {
|
|||||||
JSON.stringify(result, null, 2),
|
JSON.stringify(result, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("[page.server] Full API response:", JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
const opportunity = result?.data ?? null;
|
const opportunity = result?.data ?? null;
|
||||||
const notes = result?.data?.notes ?? [];
|
const notes = result?.data?.notes ?? [];
|
||||||
const contacts = result?.data?.contacts ?? [];
|
const contacts = result?.data?.contacts ?? [];
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
$: notes = data.notes;
|
$: notes = data.notes;
|
||||||
$: contacts = data.contacts;
|
$: contacts = data.contacts;
|
||||||
$: products = data.products;
|
$: products = data.products;
|
||||||
|
$: console.log("[OpportunityPage] full API response data:", data);
|
||||||
$: quotes = data.quotes ?? [];
|
$: quotes = data.quotes ?? [];
|
||||||
$: permissions = data.permissions;
|
$: 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 { optima } from "$lib";
|
||||||
import NoResultsMonkey from "../../../../../components/NoResultsMonkey.svelte";
|
import NoResultsMonkey from "../../../../../components/NoResultsMonkey.svelte";
|
||||||
import AddProductModal from "../../../../../components/AddProductModal.svelte";
|
import AddProductModal from "../../../../../components/AddProductModal.svelte";
|
||||||
import InventoryPopover from "../../../../../components/InventoryPopover.svelte";
|
|
||||||
import type { CatalogItem } from "$lib/optima-api/modules/procurement";
|
import type { CatalogItem } from "$lib/optima-api/modules/procurement";
|
||||||
import type {
|
import type {
|
||||||
AddProductBody,
|
AddProductBody,
|
||||||
@@ -2557,12 +2556,7 @@
|
|||||||
<div class="detail-field">
|
<div class="detail-field">
|
||||||
<span class="detail-field-label">On Hand</span>
|
<span class="detail-field-label">On Hand</span>
|
||||||
<span class="detail-field-value">
|
<span class="detail-field-value">
|
||||||
{#if selectedProduct.catalogItem}
|
{#if selectedProduct.onHand != null}
|
||||||
<InventoryPopover
|
|
||||||
identifier={String(selectedProduct.catalogItem.id)}
|
|
||||||
onHand={selectedProduct.onHand ?? undefined}
|
|
||||||
/>
|
|
||||||
{:else if selectedProduct.onHand != null}
|
|
||||||
<span
|
<span
|
||||||
class="stock-badge"
|
class="stock-badge"
|
||||||
class:stock-none={selectedProduct.onHand === 0}
|
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