fix: fixed warehouse inventory numbers and some button verbage
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { prisma } from "../../../constants";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/procurement/items/:identifier/inventory */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items/:identifier/inventory"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const includeWarehouse = c.req.query("includeWarehouse") === "true";
|
||||
const includeWarehouseBin = c.req.query("includeWarehouseBin") === "true";
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
const rows = await prisma.productInventory.findMany({
|
||||
where: { itemId: item.cwCatalogId },
|
||||
include: {
|
||||
warehouse: includeWarehouse,
|
||||
warehouseBin: includeWarehouseBin,
|
||||
},
|
||||
orderBy: [{ warehouseId: "asc" }, { warehouseBinId: "asc" }],
|
||||
});
|
||||
|
||||
const data = rows.map((row) => ({
|
||||
id: row.id,
|
||||
qtyOnHand: row.qtyOnHand,
|
||||
warehouseId: row.warehouseId,
|
||||
warehouseBinId: row.warehouseBinId,
|
||||
...(includeWarehouse && {
|
||||
warehouse: (row as any).warehouse
|
||||
? { id: (row as any).warehouse.id, name: (row as any).warehouse.name }
|
||||
: null,
|
||||
}),
|
||||
...(includeWarehouseBin && {
|
||||
warehouseBin: (row as any).warehouseBin
|
||||
? { id: (row as any).warehouseBin.id, name: (row as any).warehouseBin.name }
|
||||
: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Product inventory fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as fetch } from "./[id]/fetch";
|
||||
import { default as refreshInventory } from "./[id]/refreshInventory";
|
||||
import { default as inventoryByWarehouse } from "./[id]/inventoryByWarehouse";
|
||||
import { default as link } from "./[id]/link";
|
||||
import { default as unlink } from "./[id]/unlink";
|
||||
import { default as fetchLinked } from "./[id]/fetchLinked";
|
||||
@@ -15,6 +16,7 @@ export {
|
||||
fetchAll,
|
||||
fetchLinked,
|
||||
filters,
|
||||
inventoryByWarehouse,
|
||||
link,
|
||||
refreshInventory,
|
||||
unlink,
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
OpportunityStatus,
|
||||
StatusIdToKey,
|
||||
} from "../../../../workflows/wf.opportunity";
|
||||
import { resolveCwProbabilityId } from "../../../../modules/cw-utils/opportunities/cwProbabilityCache";
|
||||
|
||||
const updateSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).optional(),
|
||||
notes: z.string().optional(),
|
||||
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
|
||||
probability: z.number().min(0).max(100).optional(),
|
||||
rating: z.object({ id: z.number() }).optional(),
|
||||
type: z.object({ id: z.number() }).optional(),
|
||||
stage: z.object({ id: z.number() }).optional(),
|
||||
@@ -66,7 +68,19 @@ export default createRoute(
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await item.updateOpportunity(data);
|
||||
const { probability: probabilityPercent, ...rest } = data;
|
||||
|
||||
// Resolve numeric probability → CW reference ID
|
||||
let probabilityRef: { id: number } | undefined;
|
||||
if (probabilityPercent !== undefined) {
|
||||
const probId = await resolveCwProbabilityId(probabilityPercent);
|
||||
if (probId != null) probabilityRef = { id: probId };
|
||||
}
|
||||
|
||||
const updated = await item.updateOpportunity({
|
||||
...rest,
|
||||
...(probabilityRef !== undefined ? { probability: probabilityRef } : {}),
|
||||
});
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity updated successfully!",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { timeEntries } from "../../../../../managers/timeEntries";
|
||||
import { activityCw } from "../../../../../modules/cw-utils/activities/activities";
|
||||
import { ActivityController } from "../../../../../controllers/ActivityController";
|
||||
import { OptimaType } from "../../../../../workflows/wf.opportunity";
|
||||
import { prisma } from "../../../../../constants";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// HELPERS
|
||||
@@ -168,13 +169,47 @@ export default createRoute(
|
||||
}),
|
||||
);
|
||||
|
||||
// Resolve CwMember info for all unique memberIds across time entries
|
||||
const allMemberIds = Array.from(
|
||||
new Set(
|
||||
activitiesWithTimeEntries.flatMap((item) =>
|
||||
item.timeEntries
|
||||
.map((te) => te.memberId)
|
||||
.filter((id): id is string => !!id),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const memberRecords = allMemberIds.length
|
||||
? await prisma.cwMember.findMany({
|
||||
where: { identifier: { in: allMemberIds } },
|
||||
select: { identifier: true, firstName: true, lastName: true, officeEmail: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const memberMap = new Map(
|
||||
memberRecords.map((m) => [
|
||||
m.identifier,
|
||||
{ name: `${m.firstName} ${m.lastName}`.trim(), email: m.officeEmail ?? null },
|
||||
]),
|
||||
);
|
||||
|
||||
// Attach member info to each time entry
|
||||
const enrichedActivities = activitiesWithTimeEntries.map((item) => ({
|
||||
...item,
|
||||
timeEntries: item.timeEntries.map((te) => ({
|
||||
...te,
|
||||
member: te.memberId ? (memberMap.get(te.memberId) ?? null) : null,
|
||||
})),
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Workflow history fetched successfully.",
|
||||
{
|
||||
opportunityId: opportunity.id,
|
||||
cwOpportunityId: opportunity.cwOpportunityId,
|
||||
totalActivities: activitiesWithTimeEntries.length,
|
||||
activities: activitiesWithTimeEntries,
|
||||
totalActivities: enrichedActivities.length,
|
||||
activities: enrichedActivities,
|
||||
},
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createWorkflowActivity,
|
||||
OptimaType,
|
||||
} from "../../../workflows/wf.opportunity";
|
||||
import { resolveCwProbabilityId } from "../../../modules/cw-utils/opportunities/cwProbabilityCache";
|
||||
|
||||
const createSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -46,16 +47,25 @@ export default createRoute(
|
||||
const data = createSchema.parse(body);
|
||||
const { interest, ...cwCreateData } = data;
|
||||
|
||||
try {
|
||||
const item = await opportunities.createItem(cwCreateData);
|
||||
// Resolve the CW probability reference ID for 50% (the default)
|
||||
const defaultProbabilityId = await resolveCwProbabilityId(50);
|
||||
|
||||
if (interest !== undefined) {
|
||||
await prisma.opportunity.update({
|
||||
where: { uid: item.id },
|
||||
data: { interest },
|
||||
});
|
||||
item.interest = interest;
|
||||
}
|
||||
try {
|
||||
const item = await opportunities.createItem({
|
||||
...cwCreateData,
|
||||
...(defaultProbabilityId != null
|
||||
? { probability: { id: defaultProbabilityId } }
|
||||
: {}),
|
||||
});
|
||||
|
||||
// Apply defaults: "HOT" interest and 50% probability if not explicitly provided.
|
||||
const effectiveInterest = interest !== undefined ? interest : "HOT";
|
||||
await prisma.opportunity.update({
|
||||
where: { uid: item.id },
|
||||
data: { interest: effectiveInterest, probability: 50 },
|
||||
});
|
||||
item.interest = effectiveInterest;
|
||||
item.probability = 50;
|
||||
|
||||
// Create a workflow activity for the new opportunity
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CatalogManufacturer,
|
||||
} from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { catalogCw } from "../modules/cw-utils/procurement/catalog";
|
||||
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
@@ -129,7 +128,11 @@ export class CatalogItemController {
|
||||
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||
*/
|
||||
public async refreshInventory(): Promise<CatalogItemController> {
|
||||
const onHand = await catalogCw.fetchInventoryOnHand(this.cwCatalogId);
|
||||
const result = await prisma.productInventory.aggregate({
|
||||
where: { itemId: this.cwCatalogId },
|
||||
_sum: { qtyOnHand: true },
|
||||
});
|
||||
const onHand = result._sum.qtyOnHand ?? 0;
|
||||
|
||||
if (onHand !== this.onHand) {
|
||||
await prisma.catalogItem.update({
|
||||
|
||||
@@ -1904,7 +1904,7 @@ export class OpportunityController {
|
||||
expectedSalesTaxRate:
|
||||
this.taxCodeRate !== null ? this.taxCodeRate * 100 : null,
|
||||
taxCodeDescription: this.taxCodeDescription,
|
||||
probability: this.probability,
|
||||
probability: this.probability != null ? { percent: this.probability } : null,
|
||||
location: this.locationCwId
|
||||
? { id: this.locationCwId, name: this.locationName }
|
||||
: null,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* cwProbabilityCache
|
||||
*
|
||||
* Fetches and caches the list of ConnectWise probability dropdown options.
|
||||
* Used to resolve a numeric percent (0–100) to the CW probability reference ID
|
||||
* required when creating or updating opportunities via the REST API.
|
||||
*
|
||||
* CW endpoint: GET /sales/probabilities
|
||||
* Returns: [{ id: number, probability: number }]
|
||||
*/
|
||||
|
||||
import { connectWiseApi } from "../../../constants";
|
||||
|
||||
interface CWProbability {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the CW probability reference ID for a given percent value.
|
||||
*
|
||||
* Finds an exact match first; if none, returns the closest option.
|
||||
* Returns null if the probabilities list is empty.
|
||||
*/
|
||||
export async function resolveCwProbabilityId(
|
||||
percent: number
|
||||
): Promise<number | 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/** Clear the cache (useful for tests or forced refresh). */
|
||||
export function clearCwProbabilityCache(): void {
|
||||
_cache = null;
|
||||
}
|
||||
@@ -272,6 +272,7 @@ export interface CWOpportunityUpdate {
|
||||
stage?: { id: number };
|
||||
status?: { id: number };
|
||||
priority?: { id: number };
|
||||
probability?: { id: number };
|
||||
campaign?: { id: number };
|
||||
primarySalesRep?: { id: number };
|
||||
secondarySalesRep?: { id: number } | null;
|
||||
@@ -296,6 +297,7 @@ export interface CWOpportunityCreate {
|
||||
stage?: { id: number };
|
||||
status?: { id: number };
|
||||
priority?: { id: number };
|
||||
probability?: { id: number };
|
||||
campaign?: { id: number };
|
||||
secondarySalesRep?: { id: number } | null;
|
||||
site?: { id: number } | null;
|
||||
|
||||
Reference in New Issue
Block a user