feat: sales activities, forecast products, catalog categories, member cache, procurement filters, and comprehensive tests
New features: - ActivityController and manager for CW sales activities (CRUD) - ForecastProductController for opportunity forecast/product lines - CW member cache with dual-layer (in-memory + Redis) resolution - Catalog category/subcategory/ecosystem taxonomy module - Quote statuses type definitions with CW mapping - User-defined fields (UDF) module with cache and event refresh - Company sites CW module with serialization - Procurement manager filters (category, ecosystem, manufacturer, price, stock) - Opportunity notes CRUD and product line management via CW API - Opportunity type definitions endpoint Updates: - OpportunityController: CW refresh, company hydration, activities, custom fields - UserController: cwIdentifier field for CW member linking - CatalogItemController: category/subcategory fields from CW - PermissionNodes: sales note/product CRUD nodes, subCategories, collectPermissions - API routes: procurement categories/filters, sales notes/products, opportunity types - Global events: UDF and member refresh intervals on startup Tests (414 passing): - ActivityController, ForecastProductController, OpportunityController unit tests - UserController cwIdentifier tests - catalogCategories, companySites, memberCache, procurement module tests - activityTypes, opportunityTypes, quoteStatuses type tests - permissionNodes subCategories and getAllPermissionNodes tests - Updated test setup with redis mock, API method mocks, and builder helpers
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Quick utility to fetch all distinct categories, subcategories, and manufacturers
|
||||
* from the ConnectWise catalog and print them for reference.
|
||||
*/
|
||||
import { connectWiseApi } from "../src/constants";
|
||||
|
||||
interface CWReference {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CatalogItem {
|
||||
id: number;
|
||||
identifier: string;
|
||||
description: string;
|
||||
category: CWReference;
|
||||
subcategory: CWReference;
|
||||
manufacturer: CWReference;
|
||||
inactiveFlag: boolean;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const pageSize = 1000;
|
||||
|
||||
// Get total count
|
||||
const countRes = await connectWiseApi.get("/procurement/catalog/count");
|
||||
const totalCount = countRes.data.count;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
console.log(`Total catalog items: ${totalCount}`);
|
||||
|
||||
const categories = new Map<number, string>();
|
||||
const subcategories = new Map<
|
||||
number,
|
||||
{ name: string; categoryId: number; categoryName: string }
|
||||
>();
|
||||
const manufacturers = new Map<number, string>();
|
||||
const catSubcatPairs = new Map<
|
||||
string,
|
||||
{ category: string; subcategory: string; count: number }
|
||||
>();
|
||||
|
||||
for (let page = 1; page <= totalPages; page++) {
|
||||
const res = await connectWiseApi.get(
|
||||
`/procurement/catalog?page=${page}&pageSize=${pageSize}&fields=id,identifier,description,category,subcategory,manufacturer,inactiveFlag`,
|
||||
);
|
||||
const items: CatalogItem[] = res.data;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.category) {
|
||||
categories.set(item.category.id, item.category.name);
|
||||
}
|
||||
if (item.subcategory) {
|
||||
subcategories.set(item.subcategory.id, {
|
||||
name: item.subcategory.name,
|
||||
categoryId: item.category?.id,
|
||||
categoryName: item.category?.name,
|
||||
});
|
||||
}
|
||||
if (item.manufacturer) {
|
||||
manufacturers.set(item.manufacturer.id, item.manufacturer.name);
|
||||
}
|
||||
|
||||
const key = `${item.category?.name ?? "None"}::${item.subcategory?.name ?? "None"}`;
|
||||
const existing = catSubcatPairs.get(key);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
catSubcatPairs.set(key, {
|
||||
category: item.category?.name ?? "None",
|
||||
subcategory: item.subcategory?.name ?? "None",
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n=== CATEGORIES ===");
|
||||
const sortedCats = [...categories.entries()].sort((a, b) =>
|
||||
a[1].localeCompare(b[1]),
|
||||
);
|
||||
for (const [id, name] of sortedCats) {
|
||||
console.log(` [${id}] ${name}`);
|
||||
}
|
||||
|
||||
console.log("\n=== SUBCATEGORIES (grouped by category) ===");
|
||||
const groupedSubs = new Map<string, { id: number; name: string }[]>();
|
||||
for (const [id, sub] of subcategories) {
|
||||
const catName = sub.categoryName ?? "None";
|
||||
if (!groupedSubs.has(catName)) groupedSubs.set(catName, []);
|
||||
groupedSubs.get(catName)!.push({ id, name: sub.name });
|
||||
}
|
||||
for (const [catName, subs] of [...groupedSubs.entries()].sort((a, b) =>
|
||||
a[0].localeCompare(b[0]),
|
||||
)) {
|
||||
console.log(`\n ${catName}:`);
|
||||
for (const sub of subs.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
console.log(` [${sub.id}] ${sub.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n=== MANUFACTURERS ===");
|
||||
const sortedMfgs = [...manufacturers.entries()].sort((a, b) =>
|
||||
a[1].localeCompare(b[1]),
|
||||
);
|
||||
for (const [id, name] of sortedMfgs) {
|
||||
console.log(` [${id}] ${name}`);
|
||||
}
|
||||
|
||||
console.log("\n=== CATEGORY → SUBCATEGORY PAIRS (with item counts) ===");
|
||||
const sortedPairs = [...catSubcatPairs.values()].sort(
|
||||
(a, b) =>
|
||||
a.category.localeCompare(b.category) ||
|
||||
a.subcategory.localeCompare(b.subcategory),
|
||||
);
|
||||
for (const pair of sortedPairs) {
|
||||
console.log(
|
||||
` ${pair.category} → ${pair.subcategory} (${pair.count} items)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user