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 ────────────────────────────────────────────
|
||||
|
||||
@@ -182,9 +182,9 @@ export default createRoute(
|
||||
|
||||
const memberRecords = allMemberIds.length
|
||||
? await prisma.cwMember.findMany({
|
||||
where: { identifier: { in: allMemberIds } },
|
||||
select: { identifier: true, firstName: true, lastName: true, officeEmail: true },
|
||||
})
|
||||
where: { identifier: { in: allMemberIds } },
|
||||
select: { identifier: true, firstName: true, lastName: true, officeEmail: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const memberMap = new Map(
|
||||
|
||||
@@ -37,11 +37,11 @@ async function resolveMember(identifier: string | null | undefined) {
|
||||
});
|
||||
return member
|
||||
? {
|
||||
id: member.id,
|
||||
identifier: member.identifier,
|
||||
name: `${member.firstName} ${member.lastName}`.trim(),
|
||||
cwMemberId: member.cwMemberId,
|
||||
}
|
||||
id: member.id,
|
||||
identifier: member.identifier,
|
||||
name: `${member.firstName} ${member.lastName}`.trim(),
|
||||
cwMemberId: member.cwMemberId,
|
||||
}
|
||||
: { id: null, identifier, name: identifier, cwMemberId: null };
|
||||
}
|
||||
|
||||
@@ -193,8 +193,8 @@ export class OpportunityController {
|
||||
});
|
||||
const resolvedName = user
|
||||
? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() ||
|
||||
user.login ||
|
||||
user.email
|
||||
user.login ||
|
||||
user.email
|
||||
: null;
|
||||
const name =
|
||||
resolvedName ??
|
||||
@@ -209,8 +209,8 @@ export class OpportunityController {
|
||||
constructor(
|
||||
data: Opportunity & {
|
||||
company?:
|
||||
| (Company & { contacts?: any[]; companyAddresses?: any[] })
|
||||
| null;
|
||||
| (Company & { contacts?: any[]; companyAddresses?: any[] })
|
||||
| null;
|
||||
primarySalesRep?: (User & { roles: Role[] }) | null;
|
||||
secondarySalesRep?: (User & { roles: Role[] }) | null;
|
||||
},
|
||||
@@ -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;
|
||||
@@ -515,8 +513,8 @@ export class OpportunityController {
|
||||
const hasCwPatch = Object.keys(cwPatch).length > 0;
|
||||
const cwMapped = hasCwPatch
|
||||
? OpportunityController.mapCwToDb(
|
||||
await opportunityCw.update(this.cwOpportunityId, cwPatch)
|
||||
)
|
||||
await opportunityCw.update(this.cwOpportunityId, cwPatch)
|
||||
)
|
||||
: {};
|
||||
|
||||
const mapped =
|
||||
@@ -692,10 +690,10 @@ export class OpportunityController {
|
||||
},
|
||||
company: contact.company
|
||||
? {
|
||||
id: contact.company.id,
|
||||
identifier: null,
|
||||
name: contact.company.name,
|
||||
}
|
||||
id: contact.company.id,
|
||||
identifier: null,
|
||||
name: contact.company.name,
|
||||
}
|
||||
: null,
|
||||
role: null,
|
||||
notes: null,
|
||||
@@ -711,10 +709,10 @@ export class OpportunityController {
|
||||
contact: ct.contact ? { id: ct.contact.id, name: ct.contact.name } : null,
|
||||
company: ct.company
|
||||
? {
|
||||
id: ct.company.id,
|
||||
identifier: ct.company.identifier,
|
||||
name: ct.company.name,
|
||||
}
|
||||
id: ct.company.id,
|
||||
identifier: ct.company.identifier,
|
||||
name: ct.company.name,
|
||||
}
|
||||
: null,
|
||||
role: ct.role ? { id: ct.role.id, name: ct.role.name } : null,
|
||||
notes: ct.notes,
|
||||
@@ -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]));
|
||||
@@ -1056,7 +1056,7 @@ export class OpportunityController {
|
||||
|
||||
const quoteNarrativeField = options.includeQuoteNarrative
|
||||
? this._customFields?.find((f) => f.id === 35)?.value?.toString() ||
|
||||
undefined
|
||||
undefined
|
||||
: undefined;
|
||||
|
||||
// Fall back to the customerDescription of a QUO-Narrative product
|
||||
@@ -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
|
||||
@@ -1264,11 +1262,11 @@ export class OpportunityController {
|
||||
companyName: this.companyName ?? company?.name ?? null,
|
||||
primaryContact: companyJson?.cw_Data?.primaryContact
|
||||
? {
|
||||
firstName: companyJson.cw_Data.primaryContact.firstName ?? null,
|
||||
lastName: companyJson.cw_Data.primaryContact.lastName ?? null,
|
||||
email: companyJson.cw_Data.primaryContact.email ?? null,
|
||||
phone: companyJson.cw_Data.primaryContact.phone ?? null,
|
||||
}
|
||||
firstName: companyJson.cw_Data.primaryContact.firstName ?? null,
|
||||
lastName: companyJson.cw_Data.primaryContact.lastName ?? null,
|
||||
email: companyJson.cw_Data.primaryContact.email ?? null,
|
||||
phone: companyJson.cw_Data.primaryContact.phone ?? null,
|
||||
}
|
||||
: null,
|
||||
siteAddress: siteAddress.length > 0 ? siteAddress : null,
|
||||
companyAddress: companyAddress.length > 0 ? companyAddress : null,
|
||||
@@ -1820,13 +1818,13 @@ export class OpportunityController {
|
||||
const defaultAddr = this._company?.getDefaultAddress();
|
||||
const companyAddress = defaultAddr
|
||||
? {
|
||||
line1: defaultAddr.addressLine1,
|
||||
line2: defaultAddr.addressLine2,
|
||||
city: defaultAddr.city,
|
||||
state: defaultAddr.state,
|
||||
zip: defaultAddr.zipCode,
|
||||
country: defaultAddr.country ?? null,
|
||||
}
|
||||
line1: defaultAddr.addressLine1,
|
||||
line2: defaultAddr.addressLine2,
|
||||
city: defaultAddr.city,
|
||||
state: defaultAddr.state,
|
||||
zip: defaultAddr.zipCode,
|
||||
country: defaultAddr.country ?? null,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
@@ -1853,52 +1851,52 @@ export class OpportunityController {
|
||||
primarySalesRep:
|
||||
this.primarySalesRepIdentifier || this._primarySalesRep
|
||||
? {
|
||||
id: this.primarySalesRepCwId,
|
||||
identifier: this.primarySalesRepIdentifier,
|
||||
name:
|
||||
this._primarySalesRep?.name ??
|
||||
this.primarySalesRepName ??
|
||||
this.primarySalesRepIdentifier,
|
||||
...(this._primarySalesRep
|
||||
? { user: this._primarySalesRep.toJson({ safeReturn: true }) }
|
||||
: {}),
|
||||
}
|
||||
id: this.primarySalesRepCwId,
|
||||
identifier: this.primarySalesRepIdentifier,
|
||||
name:
|
||||
this._primarySalesRep?.name ??
|
||||
this.primarySalesRepName ??
|
||||
this.primarySalesRepIdentifier,
|
||||
...(this._primarySalesRep
|
||||
? { user: this._primarySalesRep.toJson({ safeReturn: true }) }
|
||||
: {}),
|
||||
}
|
||||
: null,
|
||||
secondarySalesRep:
|
||||
this.secondarySalesRepIdentifier || this._secondarySalesRep
|
||||
? {
|
||||
id: this.secondarySalesRepCwId,
|
||||
identifier: this.secondarySalesRepIdentifier,
|
||||
name:
|
||||
this._secondarySalesRep?.name ??
|
||||
this.secondarySalesRepName ??
|
||||
this.secondarySalesRepIdentifier,
|
||||
...(this._secondarySalesRep
|
||||
? {
|
||||
user: this._secondarySalesRep.toJson({
|
||||
safeReturn: true,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
id: this.secondarySalesRepCwId,
|
||||
identifier: this.secondarySalesRepIdentifier,
|
||||
name:
|
||||
this._secondarySalesRep?.name ??
|
||||
this.secondarySalesRepName ??
|
||||
this.secondarySalesRepIdentifier,
|
||||
...(this._secondarySalesRep
|
||||
? {
|
||||
user: this._secondarySalesRep.toJson({
|
||||
safeReturn: true,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: null,
|
||||
company: this._company
|
||||
? this._company.toJson({
|
||||
includeAllContacts: true,
|
||||
includeAddress: true,
|
||||
includePrimaryContact: false,
|
||||
})
|
||||
includeAllContacts: true,
|
||||
includeAddress: true,
|
||||
includePrimaryContact: false,
|
||||
})
|
||||
: this.companyCwId
|
||||
? { id: this.companyCwId, name: this.companyName }
|
||||
: null,
|
||||
? { id: this.companyCwId, name: this.companyName }
|
||||
: null,
|
||||
contact: this.contactCwId
|
||||
? { id: this.contactCwId, name: this.contactName }
|
||||
: null,
|
||||
site: this._siteData
|
||||
? this._siteData
|
||||
: this.siteCwId
|
||||
? { id: this.siteCwId, name: this.siteName }
|
||||
: null,
|
||||
? { id: this.siteCwId, name: this.siteName }
|
||||
: null,
|
||||
customerPO: this.customerPO,
|
||||
totalSalesTax: this.totalSalesTax,
|
||||
expectedSalesTaxRate:
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
import { connectWiseApi } from "../../../constants";
|
||||
|
||||
interface CWProbability {
|
||||
id: number;
|
||||
probability: number;
|
||||
id: number;
|
||||
probability: number;
|
||||
}
|
||||
|
||||
let _cache: CWProbability[] | null = null;
|
||||
|
||||
async function fetchProbabilities(): Promise<CWProbability[]> {
|
||||
if (_cache) return _cache;
|
||||
const response = await connectWiseApi.get<CWProbability[]>(
|
||||
"/sales/probabilities",
|
||||
{ params: { pageSize: 1000 } }
|
||||
);
|
||||
_cache = response.data;
|
||||
return _cache;
|
||||
if (_cache) return _cache;
|
||||
const response = await connectWiseApi.get<CWProbability[]>(
|
||||
"/sales/probabilities",
|
||||
{ params: { pageSize: 1000 } }
|
||||
);
|
||||
_cache = response.data;
|
||||
return _cache;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,29 +35,29 @@ async function fetchProbabilities(): Promise<CWProbability[]> {
|
||||
* Returns null if the probabilities list is empty.
|
||||
*/
|
||||
export async function resolveCwProbabilityId(
|
||||
percent: number
|
||||
percent: number
|
||||
): Promise<number | null> {
|
||||
const list = await fetchProbabilities();
|
||||
if (list.length === 0) return null;
|
||||
const list = await fetchProbabilities();
|
||||
if (list.length === 0) return null;
|
||||
|
||||
// Exact match
|
||||
const exact = list.find((p) => p.probability === percent);
|
||||
if (exact) return exact.id;
|
||||
// Exact match
|
||||
const exact = list.find((p) => p.probability === percent);
|
||||
if (exact) return exact.id;
|
||||
|
||||
// Closest match
|
||||
let closest = list[0]!;
|
||||
let minDiff = Math.abs(closest.probability - percent);
|
||||
for (const option of list) {
|
||||
const diff = Math.abs(option.probability - percent);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closest = option;
|
||||
// Closest match
|
||||
let closest = list[0]!;
|
||||
let minDiff = Math.abs(closest.probability - percent);
|
||||
for (const option of list) {
|
||||
const diff = Math.abs(option.probability - percent);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closest = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
return closest.id;
|
||||
return closest.id;
|
||||
}
|
||||
|
||||
/** Clear the cache (useful for tests or forced refresh). */
|
||||
export function clearCwProbabilityCache(): void {
|
||||
_cache = null;
|
||||
_cache = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user