Add sales item labor/product route updates and permission docs
This commit is contained in:
@@ -782,6 +782,41 @@ export class OpportunityController {
|
||||
return this.fetchProducts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Append Product Sequence IDs
|
||||
*
|
||||
* Adds newly created forecast item IDs to the end of the local
|
||||
* productSequence array, preserving existing order and avoiding duplicates.
|
||||
*/
|
||||
private async appendProductSequenceIds(ids: number[]): Promise<void> {
|
||||
const normalizedIds = ids.filter(
|
||||
(id): id is number => Number.isInteger(id) && id > 0,
|
||||
);
|
||||
if (normalizedIds.length === 0) return;
|
||||
|
||||
const current = await prisma.opportunity.findUnique({
|
||||
where: { id: this.id },
|
||||
select: { productSequence: true },
|
||||
});
|
||||
|
||||
const existing = current?.productSequence ?? [];
|
||||
const existingSet = new Set(existing);
|
||||
const idsToAppend = normalizedIds.filter((id) => !existingSet.has(id));
|
||||
if (idsToAppend.length === 0) {
|
||||
this.productSequence = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSequence = [...existing, ...idsToAppend];
|
||||
|
||||
await prisma.opportunity.update({
|
||||
where: { id: this.id },
|
||||
data: { productSequence: updatedSequence },
|
||||
});
|
||||
|
||||
this.productSequence = updatedSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Products
|
||||
*
|
||||
@@ -800,6 +835,7 @@ export class OpportunityController {
|
||||
this.cwOpportunityId,
|
||||
data,
|
||||
);
|
||||
await this.appendProductSequenceIds(created.map((item) => item.id));
|
||||
await invalidateProductsCache(this.cwOpportunityId);
|
||||
return created.map((item) => new ForecastProductController(item));
|
||||
} catch (err: any) {
|
||||
@@ -845,6 +881,11 @@ export class OpportunityController {
|
||||
}));
|
||||
|
||||
const created = await opportunityCw.createProcurementProducts(normalized);
|
||||
await this.appendProductSequenceIds(
|
||||
created
|
||||
.map((item) => item.forecastDetailId)
|
||||
.filter((id): id is number => typeof id === "number"),
|
||||
);
|
||||
await invalidateProductsCache(this.cwOpportunityId);
|
||||
return created;
|
||||
} catch (err: any) {
|
||||
@@ -873,6 +914,91 @@ export class OpportunityController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Procurement Product By Forecast Item
|
||||
*
|
||||
* Returns the linked procurement product for a forecast item ID,
|
||||
* or null when no procurement record exists.
|
||||
*/
|
||||
public async fetchProcurementProductByForecastItem(
|
||||
forecastItemId: number,
|
||||
): Promise<CWProcurementProduct | null> {
|
||||
return opportunityCw.fetchProcurementProductByForecastDetail(
|
||||
this.cwOpportunityId,
|
||||
forecastItemId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Procurement Product By Forecast Item
|
||||
*
|
||||
* Finds the linked procurement product for a forecast item and updates it.
|
||||
* Returns null when no linked procurement product exists.
|
||||
*/
|
||||
public async updateProcurementProductByForecastItem(
|
||||
forecastItemId: number,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<CWProcurementProduct | null> {
|
||||
const linked =
|
||||
await this.fetchProcurementProductByForecastItem(forecastItemId);
|
||||
if (!linked?.id) return null;
|
||||
|
||||
const updated = await opportunityCw.updateProcurementProduct(
|
||||
linked.id,
|
||||
data,
|
||||
);
|
||||
await invalidateProductsCache(this.cwOpportunityId);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Product Cancellation
|
||||
*
|
||||
* Updates cancellation fields on the procurement product linked to a
|
||||
* forecast item. A quantity of 0 is treated as uncancelled.
|
||||
*/
|
||||
public async setProductCancellation(
|
||||
forecastItemId: number,
|
||||
opts: { quantityCancelled: number; cancellationReason?: string | null },
|
||||
): Promise<CWProcurementProduct> {
|
||||
const linked =
|
||||
await this.fetchProcurementProductByForecastItem(forecastItemId);
|
||||
|
||||
if (!linked?.id) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ProcurementProductNotFound",
|
||||
message:
|
||||
"No linked procurement product found for the specified forecast item",
|
||||
});
|
||||
}
|
||||
|
||||
const quantityCancelled = Math.max(0, Math.trunc(opts.quantityCancelled));
|
||||
const cancelledFlag = quantityCancelled > 0;
|
||||
|
||||
const updated = await this.updateProcurementProductByForecastItem(
|
||||
forecastItemId,
|
||||
{
|
||||
quantityCancelled,
|
||||
cancelledFlag,
|
||||
cancelledReason: cancelledFlag
|
||||
? (opts.cancellationReason ?? null)
|
||||
: null,
|
||||
},
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ProcurementProductNotFound",
|
||||
message:
|
||||
"No linked procurement product found for the specified forecast item",
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Note
|
||||
*
|
||||
@@ -938,7 +1064,7 @@ export class OpportunityController {
|
||||
id: this.id,
|
||||
cwOpportunityId: this.cwOpportunityId,
|
||||
name: this.name,
|
||||
notes: this.notes,
|
||||
description: this.notes,
|
||||
type: this.typeCwId ? { id: this.typeCwId, name: this.typeName } : null,
|
||||
stage: this.stageCwId
|
||||
? { id: this.stageCwId, name: this.stageName }
|
||||
|
||||
Reference in New Issue
Block a user