Fix UserController permission serialization and include current updates
This commit is contained in:
+673
@@ -2204,6 +2204,679 @@ A fun Easter egg endpoint that returns HTTP 418 (I'm a teapot).
|
||||
|
||||
---
|
||||
|
||||
## Procurement Routes
|
||||
|
||||
### Get All Catalog Items
|
||||
|
||||
**GET** `/procurement/items`
|
||||
|
||||
Fetch a paginated list of catalog items. Supports search.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `page` (optional, default `1`) — Page number
|
||||
- `rpp` (optional, default `30`) — Records per page
|
||||
- `search` (optional) — Search by name, description, part number, vendor SKU, or manufacturer
|
||||
- `includeInactive` (optional, default `false`) — Include inactive catalog items in results
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog items fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"description": "Dell OptiPlex 7020 SFF Desktop",
|
||||
"customerDescription": "Business Desktop Computer",
|
||||
"internalNotes": null,
|
||||
"manufacturer": "Dell",
|
||||
"manufactureCwId": 45,
|
||||
"partNumber": "OPT7020-SFF",
|
||||
"vendorName": "Dell Direct",
|
||||
"vendorSku": "DELL-OPT7020",
|
||||
"vendorCwId": 12,
|
||||
"price": 899.99,
|
||||
"cost": 650.0,
|
||||
"inactive": false,
|
||||
"salesTaxable": true,
|
||||
"onHand": 5,
|
||||
"cwLastUpdated": "2026-02-25T10:00:00.000Z",
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-25T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"previousPage": null,
|
||||
"currentPage": 1,
|
||||
"nextPage": 2,
|
||||
"totalPages": 10,
|
||||
"totalRecords": 300,
|
||||
"listedRecords": 30
|
||||
}
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Catalog Item
|
||||
|
||||
**GET** `/procurement/items/:identifier`
|
||||
|
||||
Fetch a single catalog item by its internal ID or ConnectWise catalog ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise catalog ID (numeric)
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `includeLinkedItems` (optional, default `false`) — Include linked catalog items in the response
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item fetched successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"description": "Dell OptiPlex 7020 SFF Desktop",
|
||||
"customerDescription": "Business Desktop Computer",
|
||||
"internalNotes": null,
|
||||
"manufacturer": "Dell",
|
||||
"manufactureCwId": 45,
|
||||
"partNumber": "OPT7020-SFF",
|
||||
"vendorName": "Dell Direct",
|
||||
"vendorSku": "DELL-OPT7020",
|
||||
"vendorCwId": 12,
|
||||
"price": 899.99,
|
||||
"cost": 650.0,
|
||||
"inactive": false,
|
||||
"salesTaxable": true,
|
||||
"onHand": 5,
|
||||
"cwLastUpdated": "2026-02-25T10:00:00.000Z",
|
||||
"linkedItems": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 456,
|
||||
"name": "Dell Warranty - 3 Year"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-25T10:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Catalog Item Count
|
||||
|
||||
**GET** `/procurement/count`
|
||||
|
||||
Get the total number of catalog items.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `activeOnly` (optional, default `false`) — Only count active (non-inactive) items
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item count fetched successfully!",
|
||||
"data": {
|
||||
"count": 300
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Refresh Catalog Item Inventory
|
||||
|
||||
**POST** `/procurement/items/:identifier/refresh-inventory`
|
||||
|
||||
Refresh the on-hand inventory count for a catalog item by fetching the latest data from ConnectWise.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.inventory.refresh`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise catalog ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Inventory refreshed successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"onHand": 7,
|
||||
"price": 899.99,
|
||||
"cost": 650.0,
|
||||
"inactive": false,
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T12:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Linked Catalog Items
|
||||
|
||||
**GET** `/procurement/items/:identifier/linked`
|
||||
|
||||
Fetch all catalog items linked to a specific item.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Linked catalog items fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 456,
|
||||
"identifier": "DELL-WAR-3YR",
|
||||
"name": "Dell Warranty - 3 Year",
|
||||
"description": "Dell 3 Year ProSupport Warranty",
|
||||
"price": 199.99,
|
||||
"cost": 120.0,
|
||||
"inactive": false,
|
||||
"onHand": 0,
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-25T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Link Catalog Items
|
||||
|
||||
**POST** `/procurement/items/:identifier/link`
|
||||
|
||||
Link a target catalog item to the specified source item. The source item is identified by the URL parameter and the target by the request body.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.link`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) of the source item
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"targetId": "clx..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item linked successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"identifier": "OPT7020-SFF",
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"linkedItems": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 456,
|
||||
"identifier": "DELL-WAR-3YR",
|
||||
"name": "Dell Warranty - 3 Year"
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T12:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Unlink Catalog Items
|
||||
|
||||
**POST** `/procurement/items/:identifier/unlink`
|
||||
|
||||
Remove the link between a source catalog item and a target catalog item.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `procurement.catalog.link`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) of the source item
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"targetId": "clx..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Catalog item unlinked successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwCatalogId": 123,
|
||||
"identifier": "OPT7020-SFF",
|
||||
"name": "Dell OptiPlex 7020",
|
||||
"linkedItems": [],
|
||||
"createdAt": "2026-01-15T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T12:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sales Routes
|
||||
|
||||
Sales routes serve opportunity data stored locally and synced from ConnectWise. List, search, and count operations read from the local database. Sub-resource routes (forecasts, notes, contacts) fetch live data from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
### Get All Opportunities
|
||||
|
||||
**GET** `/sales/opportunities`
|
||||
|
||||
Fetch a paginated list of opportunities. Supports search.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `page` (optional, default `1`) — Page number
|
||||
- `rpp` (optional, default `30`) — Records per page
|
||||
- `search` (optional) — Search by opportunity name
|
||||
- `includeClosed` (optional, default `false`) — Include closed opportunities in results
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunities fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Full network redesign and hardware refresh",
|
||||
"type": { "id": 1, "name": "New" },
|
||||
"stage": { "id": 3, "name": "Proposal" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": null,
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": null,
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": null,
|
||||
"totalSalesTax": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
|
||||
"pipelineChangeDate": "2026-02-20T00:00:00.000Z",
|
||||
"dateBecameLead": "2026-01-10T00:00:00.000Z",
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-02-26T10:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"previousPage": null,
|
||||
"currentPage": 1,
|
||||
"nextPage": 2,
|
||||
"totalPages": 5,
|
||||
"totalRecords": 150,
|
||||
"listedRecords": 30
|
||||
}
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Count
|
||||
|
||||
**GET** `/sales/opportunities/count`
|
||||
|
||||
Get the total number of opportunities.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch.many`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `openOnly` (optional, default `false`) — Only count open (non-closed) opportunities
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity count fetched successfully!",
|
||||
"data": {
|
||||
"count": 150
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity
|
||||
|
||||
**GET** `/sales/opportunities/:identifier`
|
||||
|
||||
Fetch a single opportunity by its internal ID or ConnectWise opportunity ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity fetched successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Full network redesign and hardware refresh",
|
||||
"type": { "id": 1, "name": "New" },
|
||||
"stage": { "id": 3, "name": "Proposal" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": null,
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": null,
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": null,
|
||||
"totalSalesTax": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
|
||||
"pipelineChangeDate": "2026-02-20T00:00:00.000Z",
|
||||
"dateBecameLead": "2026-01-10T00:00:00.000Z",
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-02-26T10:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T10:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Refresh Opportunity
|
||||
|
||||
**POST** `/sales/opportunities/:identifier/refresh`
|
||||
|
||||
Refresh an opportunity's local data by fetching the latest from ConnectWise.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.refresh`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity refreshed from ConnectWise successfully!",
|
||||
"data": {
|
||||
"id": "clx...",
|
||||
"cwOpportunityId": 456,
|
||||
"name": "Acme Corp Network Refresh",
|
||||
"notes": "Updated notes from CW",
|
||||
"type": { "id": 1, "name": "New" },
|
||||
"stage": { "id": 4, "name": "Negotiation" },
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"priority": { "id": 2, "name": "High" },
|
||||
"rating": { "id": 1, "name": "Hot" },
|
||||
"source": "Referral",
|
||||
"campaign": null,
|
||||
"primarySalesRep": {
|
||||
"id": 10,
|
||||
"identifier": "JDoe",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"secondarySalesRep": null,
|
||||
"company": { "id": 100, "name": "Acme Corp" },
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"site": { "id": 50, "name": "Main Office" },
|
||||
"customerPO": null,
|
||||
"totalSalesTax": 0,
|
||||
"location": { "id": 1, "name": "Murray" },
|
||||
"department": { "id": 5, "name": "Sales" },
|
||||
"expectedCloseDate": "2026-04-15T00:00:00.000Z",
|
||||
"pipelineChangeDate": "2026-02-25T00:00:00.000Z",
|
||||
"dateBecameLead": "2026-01-10T00:00:00.000Z",
|
||||
"closedDate": null,
|
||||
"closedFlag": false,
|
||||
"closedBy": null,
|
||||
"companyId": "clx...",
|
||||
"cwLastUpdated": "2026-02-26T14:00:00.000Z",
|
||||
"createdAt": "2026-02-01T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-26T14:00:00.000Z"
|
||||
},
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Forecasts
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/forecasts`
|
||||
|
||||
Fetch forecast/revenue items for an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity forecasts fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"forecastType": "Revenue",
|
||||
"forecastMonth": "2026-03-01T00:00:00Z",
|
||||
"revenue": 50000.0,
|
||||
"cost": 30000.0,
|
||||
"forecastPercentage": 75,
|
||||
"status": { "id": 1, "name": "Open" },
|
||||
"includedFlag": true,
|
||||
"linkedFlag": false,
|
||||
"recurringFlag": false
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Notes
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/notes`
|
||||
|
||||
Fetch notes for an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity notes fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Client expressed interest in a full network refresh.",
|
||||
"type": { "id": 2, "name": "Discussion" },
|
||||
"flagged": false,
|
||||
"enteredBy": "JDoe"
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Opportunity Contacts
|
||||
|
||||
**GET** `/sales/opportunities/:identifier/contacts`
|
||||
|
||||
Fetch contacts associated with an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID.
|
||||
|
||||
**Authentication Required:** Yes
|
||||
|
||||
**Required Permissions:** `sales.opportunity.fetch`
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "Opportunity contacts fetched successfully!",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"contact": { "id": 200, "name": "Jane Smith" },
|
||||
"company": {
|
||||
"id": 100,
|
||||
"identifier": "AcmeCorp",
|
||||
"name": "Acme Corp"
|
||||
},
|
||||
"role": { "id": 1, "name": "Decision Maker" },
|
||||
"notes": "Primary point of contact for this deal",
|
||||
"referralFlag": false
|
||||
}
|
||||
],
|
||||
"successful": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UniFi Routes
|
||||
|
||||
All UniFi routes require the `unifi.access` permission in addition to their route-specific permission. This acts as a gate for the entire UniFi API.
|
||||
|
||||
@@ -115,6 +115,25 @@ Admin-specific UI permissions that control visibility and data loading for admin
|
||||
- **Combine with API permissions**: A user with an admin UI permission should also have the corresponding API permission (e.g., `role.list`) to actually load data.
|
||||
- **Use wildcards for flexibility**: Grant `ui.navigation.*.view` to allow all navigation sections.
|
||||
|
||||
### Procurement Permissions
|
||||
|
||||
| Permission Node | Description | Used In | Dependencies |
|
||||
| --------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `procurement.catalog.fetch` | Fetch a single catalog item | [src/api/procurement/[id]/fetch.ts](src/api/procurement/[id]/fetch.ts) | |
|
||||
| `procurement.catalog.fetch.many` | Fetch multiple catalog items or count | [src/api/procurement/fetchAll.ts](src/api/procurement/fetchAll.ts), [src/api/procurement/count.ts](src/api/procurement/count.ts) | |
|
||||
| `procurement.catalog.inventory.refresh` | Refresh on-hand inventory for a catalog item from ConnectWise | [src/api/procurement/[id]/refreshInventory.ts](src/api/procurement/[id]/refreshInventory.ts) | `procurement.catalog.fetch` |
|
||||
| `procurement.catalog.link` | Link or unlink catalog items to each other | [src/api/procurement/[id]/link.ts](src/api/procurement/[id]/link.ts), [src/api/procurement/[id]/unlink.ts](src/api/procurement/[id]/unlink.ts) | `procurement.catalog.fetch` |
|
||||
|
||||
### Sales Permissions
|
||||
|
||||
Permissions for accessing and managing sales opportunities. Opportunities are synced from ConnectWise and stored locally; sub-resources (forecasts, notes, contacts) are fetched live from CW.
|
||||
|
||||
| Permission Node | Description | Used In | Dependencies |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
|
||||
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (forecasts, notes, contacts) | [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts), [src/api/sales/[id]/forecasts.ts](src/api/sales/[id]/forecasts.ts), [src/api/sales/[id]/notes.ts](src/api/sales/[id]/notes.ts), [src/api/sales/[id]/contacts.ts](src/api/sales/[id]/contacts.ts) | |
|
||||
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable) or get opportunity count | [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/count.ts](src/api/sales/count.ts) | |
|
||||
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/[id]/refresh.ts](src/api/sales/[id]/refresh.ts) | `sales.opportunity.fetch` |
|
||||
|
||||
### UniFi Permissions
|
||||
|
||||
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
|
||||
|
||||
@@ -47,6 +47,11 @@ export type Company = Prisma.CompanyModel
|
||||
*
|
||||
*/
|
||||
export type CatalogItem = Prisma.CatalogItemModel
|
||||
/**
|
||||
* Model Opportunity
|
||||
*
|
||||
*/
|
||||
export type Opportunity = Prisma.OpportunityModel
|
||||
/**
|
||||
* Model CredentialType
|
||||
*
|
||||
|
||||
@@ -69,6 +69,11 @@ export type Company = Prisma.CompanyModel
|
||||
*
|
||||
*/
|
||||
export type CatalogItem = Prisma.CatalogItemModel
|
||||
/**
|
||||
* Model Opportunity
|
||||
*
|
||||
*/
|
||||
export type Opportunity = Prisma.OpportunityModel
|
||||
/**
|
||||
* Model CredentialType
|
||||
*
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -390,6 +390,7 @@ export const ModelName = {
|
||||
UnifiSite: 'UnifiSite',
|
||||
Company: 'Company',
|
||||
CatalogItem: 'CatalogItem',
|
||||
Opportunity: 'Opportunity',
|
||||
CredentialType: 'CredentialType',
|
||||
SecureValue: 'SecureValue',
|
||||
Credential: 'Credential'
|
||||
@@ -408,7 +409,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "credentialType" | "secureValue" | "credential"
|
||||
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "opportunity" | "credentialType" | "secureValue" | "credential"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
@@ -856,6 +857,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
}
|
||||
}
|
||||
Opportunity: {
|
||||
payload: Prisma.$OpportunityPayload<ExtArgs>
|
||||
fields: Prisma.OpportunityFieldRefs
|
||||
operations: {
|
||||
findUnique: {
|
||||
args: Prisma.OpportunityFindUniqueArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
|
||||
}
|
||||
findUniqueOrThrow: {
|
||||
args: Prisma.OpportunityFindUniqueOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||
}
|
||||
findFirst: {
|
||||
args: Prisma.OpportunityFindFirstArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload> | null
|
||||
}
|
||||
findFirstOrThrow: {
|
||||
args: Prisma.OpportunityFindFirstOrThrowArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||
}
|
||||
findMany: {
|
||||
args: Prisma.OpportunityFindManyArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||
}
|
||||
create: {
|
||||
args: Prisma.OpportunityCreateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||
}
|
||||
createMany: {
|
||||
args: Prisma.OpportunityCreateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
createManyAndReturn: {
|
||||
args: Prisma.OpportunityCreateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||
}
|
||||
delete: {
|
||||
args: Prisma.OpportunityDeleteArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||
}
|
||||
update: {
|
||||
args: Prisma.OpportunityUpdateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||
}
|
||||
deleteMany: {
|
||||
args: Prisma.OpportunityDeleteManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateMany: {
|
||||
args: Prisma.OpportunityUpdateManyArgs<ExtArgs>
|
||||
result: BatchPayload
|
||||
}
|
||||
updateManyAndReturn: {
|
||||
args: Prisma.OpportunityUpdateManyAndReturnArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>[]
|
||||
}
|
||||
upsert: {
|
||||
args: Prisma.OpportunityUpsertArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.PayloadToResult<Prisma.$OpportunityPayload>
|
||||
}
|
||||
aggregate: {
|
||||
args: Prisma.OpportunityAggregateArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.AggregateOpportunity>
|
||||
}
|
||||
groupBy: {
|
||||
args: Prisma.OpportunityGroupByArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.OpportunityGroupByOutputType>[]
|
||||
}
|
||||
count: {
|
||||
args: Prisma.OpportunityCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.OpportunityCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
CredentialType: {
|
||||
payload: Prisma.$CredentialTypePayload<ExtArgs>
|
||||
fields: Prisma.CredentialTypeFieldRefs
|
||||
@@ -1186,6 +1261,7 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
|
||||
export const CatalogItemScalarFieldEnum = {
|
||||
id: 'id',
|
||||
cwCatalogId: 'cwCatalogId',
|
||||
identifier: 'identifier',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
customerDescription: 'customerDescription',
|
||||
@@ -1209,6 +1285,58 @@ export const CatalogItemScalarFieldEnum = {
|
||||
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
||||
|
||||
|
||||
export const OpportunityScalarFieldEnum = {
|
||||
id: 'id',
|
||||
cwOpportunityId: 'cwOpportunityId',
|
||||
name: 'name',
|
||||
notes: 'notes',
|
||||
typeName: 'typeName',
|
||||
typeCwId: 'typeCwId',
|
||||
stageName: 'stageName',
|
||||
stageCwId: 'stageCwId',
|
||||
statusName: 'statusName',
|
||||
statusCwId: 'statusCwId',
|
||||
priorityName: 'priorityName',
|
||||
priorityCwId: 'priorityCwId',
|
||||
ratingName: 'ratingName',
|
||||
ratingCwId: 'ratingCwId',
|
||||
source: 'source',
|
||||
campaignName: 'campaignName',
|
||||
campaignCwId: 'campaignCwId',
|
||||
primarySalesRepName: 'primarySalesRepName',
|
||||
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||
secondarySalesRepName: 'secondarySalesRepName',
|
||||
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||
companyCwId: 'companyCwId',
|
||||
companyName: 'companyName',
|
||||
contactCwId: 'contactCwId',
|
||||
contactName: 'contactName',
|
||||
siteCwId: 'siteCwId',
|
||||
siteName: 'siteName',
|
||||
customerPO: 'customerPO',
|
||||
totalSalesTax: 'totalSalesTax',
|
||||
locationName: 'locationName',
|
||||
locationCwId: 'locationCwId',
|
||||
departmentName: 'departmentName',
|
||||
departmentCwId: 'departmentCwId',
|
||||
expectedCloseDate: 'expectedCloseDate',
|
||||
pipelineChangeDate: 'pipelineChangeDate',
|
||||
dateBecameLead: 'dateBecameLead',
|
||||
closedDate: 'closedDate',
|
||||
closedFlag: 'closedFlag',
|
||||
closedByName: 'closedByName',
|
||||
closedByCwId: 'closedByCwId',
|
||||
companyId: 'companyId',
|
||||
cwLastUpdated: 'cwLastUpdated',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
|
||||
|
||||
|
||||
export const CredentialTypeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
@@ -1473,6 +1601,7 @@ export type GlobalOmitConfig = {
|
||||
unifiSite?: Prisma.UnifiSiteOmit
|
||||
company?: Prisma.CompanyOmit
|
||||
catalogItem?: Prisma.CatalogItemOmit
|
||||
opportunity?: Prisma.OpportunityOmit
|
||||
credentialType?: Prisma.CredentialTypeOmit
|
||||
secureValue?: Prisma.SecureValueOmit
|
||||
credential?: Prisma.CredentialOmit
|
||||
|
||||
@@ -57,6 +57,7 @@ export const ModelName = {
|
||||
UnifiSite: 'UnifiSite',
|
||||
Company: 'Company',
|
||||
CatalogItem: 'CatalogItem',
|
||||
Opportunity: 'Opportunity',
|
||||
CredentialType: 'CredentialType',
|
||||
SecureValue: 'SecureValue',
|
||||
Credential: 'Credential'
|
||||
@@ -147,6 +148,7 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
|
||||
export const CatalogItemScalarFieldEnum = {
|
||||
id: 'id',
|
||||
cwCatalogId: 'cwCatalogId',
|
||||
identifier: 'identifier',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
customerDescription: 'customerDescription',
|
||||
@@ -170,6 +172,58 @@ export const CatalogItemScalarFieldEnum = {
|
||||
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof CatalogItemScalarFieldEnum]
|
||||
|
||||
|
||||
export const OpportunityScalarFieldEnum = {
|
||||
id: 'id',
|
||||
cwOpportunityId: 'cwOpportunityId',
|
||||
name: 'name',
|
||||
notes: 'notes',
|
||||
typeName: 'typeName',
|
||||
typeCwId: 'typeCwId',
|
||||
stageName: 'stageName',
|
||||
stageCwId: 'stageCwId',
|
||||
statusName: 'statusName',
|
||||
statusCwId: 'statusCwId',
|
||||
priorityName: 'priorityName',
|
||||
priorityCwId: 'priorityCwId',
|
||||
ratingName: 'ratingName',
|
||||
ratingCwId: 'ratingCwId',
|
||||
source: 'source',
|
||||
campaignName: 'campaignName',
|
||||
campaignCwId: 'campaignCwId',
|
||||
primarySalesRepName: 'primarySalesRepName',
|
||||
primarySalesRepIdentifier: 'primarySalesRepIdentifier',
|
||||
primarySalesRepCwId: 'primarySalesRepCwId',
|
||||
secondarySalesRepName: 'secondarySalesRepName',
|
||||
secondarySalesRepIdentifier: 'secondarySalesRepIdentifier',
|
||||
secondarySalesRepCwId: 'secondarySalesRepCwId',
|
||||
companyCwId: 'companyCwId',
|
||||
companyName: 'companyName',
|
||||
contactCwId: 'contactCwId',
|
||||
contactName: 'contactName',
|
||||
siteCwId: 'siteCwId',
|
||||
siteName: 'siteName',
|
||||
customerPO: 'customerPO',
|
||||
totalSalesTax: 'totalSalesTax',
|
||||
locationName: 'locationName',
|
||||
locationCwId: 'locationCwId',
|
||||
departmentName: 'departmentName',
|
||||
departmentCwId: 'departmentCwId',
|
||||
expectedCloseDate: 'expectedCloseDate',
|
||||
pipelineChangeDate: 'pipelineChangeDate',
|
||||
dateBecameLead: 'dateBecameLead',
|
||||
closedDate: 'closedDate',
|
||||
closedFlag: 'closedFlag',
|
||||
closedByName: 'closedByName',
|
||||
closedByCwId: 'closedByCwId',
|
||||
companyId: 'companyId',
|
||||
cwLastUpdated: 'cwLastUpdated',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type OpportunityScalarFieldEnum = (typeof OpportunityScalarFieldEnum)[keyof typeof OpportunityScalarFieldEnum]
|
||||
|
||||
|
||||
export const CredentialTypeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
|
||||
@@ -14,6 +14,7 @@ export type * from './models/Role.ts'
|
||||
export type * from './models/UnifiSite.ts'
|
||||
export type * from './models/Company.ts'
|
||||
export type * from './models/CatalogItem.ts'
|
||||
export type * from './models/Opportunity.ts'
|
||||
export type * from './models/CredentialType.ts'
|
||||
export type * from './models/SecureValue.ts'
|
||||
export type * from './models/Credential.ts'
|
||||
|
||||
@@ -47,6 +47,7 @@ export type CatalogItemSumAggregateOutputType = {
|
||||
export type CatalogItemMinAggregateOutputType = {
|
||||
id: string | null
|
||||
cwCatalogId: number | null
|
||||
identifier: string | null
|
||||
name: string | null
|
||||
description: string | null
|
||||
customerDescription: string | null
|
||||
@@ -70,6 +71,7 @@ export type CatalogItemMinAggregateOutputType = {
|
||||
export type CatalogItemMaxAggregateOutputType = {
|
||||
id: string | null
|
||||
cwCatalogId: number | null
|
||||
identifier: string | null
|
||||
name: string | null
|
||||
description: string | null
|
||||
customerDescription: string | null
|
||||
@@ -93,6 +95,7 @@ export type CatalogItemMaxAggregateOutputType = {
|
||||
export type CatalogItemCountAggregateOutputType = {
|
||||
id: number
|
||||
cwCatalogId: number
|
||||
identifier: number
|
||||
name: number
|
||||
description: number
|
||||
customerDescription: number
|
||||
@@ -136,6 +139,7 @@ export type CatalogItemSumAggregateInputType = {
|
||||
export type CatalogItemMinAggregateInputType = {
|
||||
id?: true
|
||||
cwCatalogId?: true
|
||||
identifier?: true
|
||||
name?: true
|
||||
description?: true
|
||||
customerDescription?: true
|
||||
@@ -159,6 +163,7 @@ export type CatalogItemMinAggregateInputType = {
|
||||
export type CatalogItemMaxAggregateInputType = {
|
||||
id?: true
|
||||
cwCatalogId?: true
|
||||
identifier?: true
|
||||
name?: true
|
||||
description?: true
|
||||
customerDescription?: true
|
||||
@@ -182,6 +187,7 @@ export type CatalogItemMaxAggregateInputType = {
|
||||
export type CatalogItemCountAggregateInputType = {
|
||||
id?: true
|
||||
cwCatalogId?: true
|
||||
identifier?: true
|
||||
name?: true
|
||||
description?: true
|
||||
customerDescription?: true
|
||||
@@ -292,6 +298,7 @@ export type CatalogItemGroupByArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
||||
export type CatalogItemGroupByOutputType = {
|
||||
id: string
|
||||
cwCatalogId: number
|
||||
identifier: string | null
|
||||
name: string
|
||||
description: string | null
|
||||
customerDescription: string | null
|
||||
@@ -338,6 +345,7 @@ export type CatalogItemWhereInput = {
|
||||
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||
id?: Prisma.StringFilter<"CatalogItem"> | string
|
||||
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
||||
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||
name?: Prisma.StringFilter<"CatalogItem"> | string
|
||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||
@@ -363,6 +371,7 @@ export type CatalogItemWhereInput = {
|
||||
export type CatalogItemOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
cwCatalogId?: Prisma.SortOrder
|
||||
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
@@ -388,6 +397,7 @@ export type CatalogItemOrderByWithRelationInput = {
|
||||
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
||||
id?: string
|
||||
cwCatalogId?: number
|
||||
identifier?: string
|
||||
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||
OR?: Prisma.CatalogItemWhereInput[]
|
||||
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
|
||||
@@ -411,11 +421,12 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
|
||||
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
|
||||
linkedItems?: Prisma.CatalogItemListRelationFilter
|
||||
linkedTo?: Prisma.CatalogItemListRelationFilter
|
||||
}, "id" | "cwCatalogId">
|
||||
}, "id" | "cwCatalogId" | "identifier">
|
||||
|
||||
export type CatalogItemOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
cwCatalogId?: Prisma.SortOrder
|
||||
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
@@ -447,6 +458,7 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
|
||||
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
|
||||
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
||||
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
|
||||
identifier?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
|
||||
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
|
||||
@@ -470,6 +482,7 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
|
||||
export type CatalogItemCreateInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -495,6 +508,7 @@ export type CatalogItemCreateInput = {
|
||||
export type CatalogItemUncheckedCreateInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -520,6 +534,7 @@ export type CatalogItemUncheckedCreateInput = {
|
||||
export type CatalogItemUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -545,6 +560,7 @@ export type CatalogItemUpdateInput = {
|
||||
export type CatalogItemUncheckedUpdateInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -570,6 +586,7 @@ export type CatalogItemUncheckedUpdateInput = {
|
||||
export type CatalogItemCreateManyInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -593,6 +610,7 @@ export type CatalogItemCreateManyInput = {
|
||||
export type CatalogItemUpdateManyMutationInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -616,6 +634,7 @@ export type CatalogItemUpdateManyMutationInput = {
|
||||
export type CatalogItemUncheckedUpdateManyInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -649,6 +668,7 @@ export type CatalogItemOrderByRelationAggregateInput = {
|
||||
export type CatalogItemCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
cwCatalogId?: Prisma.SortOrder
|
||||
identifier?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrder
|
||||
customerDescription?: Prisma.SortOrder
|
||||
@@ -681,6 +701,7 @@ export type CatalogItemAvgOrderByAggregateInput = {
|
||||
export type CatalogItemMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
cwCatalogId?: Prisma.SortOrder
|
||||
identifier?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrder
|
||||
customerDescription?: Prisma.SortOrder
|
||||
@@ -704,6 +725,7 @@ export type CatalogItemMaxOrderByAggregateInput = {
|
||||
export type CatalogItemMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
cwCatalogId?: Prisma.SortOrder
|
||||
identifier?: Prisma.SortOrder
|
||||
name?: Prisma.SortOrder
|
||||
description?: Prisma.SortOrder
|
||||
customerDescription?: Prisma.SortOrder
|
||||
@@ -828,6 +850,7 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsNestedInput = {
|
||||
export type CatalogItemCreateWithoutLinkedToInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -852,6 +875,7 @@ export type CatalogItemCreateWithoutLinkedToInput = {
|
||||
export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -881,6 +905,7 @@ export type CatalogItemCreateOrConnectWithoutLinkedToInput = {
|
||||
export type CatalogItemCreateWithoutLinkedItemsInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -905,6 +930,7 @@ export type CatalogItemCreateWithoutLinkedItemsInput = {
|
||||
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
|
||||
id?: string
|
||||
cwCatalogId: number
|
||||
identifier?: string | null
|
||||
name: string
|
||||
description?: string | null
|
||||
customerDescription?: string | null
|
||||
@@ -953,6 +979,7 @@ export type CatalogItemScalarWhereInput = {
|
||||
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
|
||||
id?: Prisma.StringFilter<"CatalogItem"> | string
|
||||
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
|
||||
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||
name?: Prisma.StringFilter<"CatalogItem"> | string
|
||||
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
|
||||
@@ -992,6 +1019,7 @@ export type CatalogItemUpdateManyWithWhereWithoutLinkedItemsInput = {
|
||||
export type CatalogItemUpdateWithoutLinkedToInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -1016,6 +1044,7 @@ export type CatalogItemUpdateWithoutLinkedToInput = {
|
||||
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -1040,6 +1069,7 @@ export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
|
||||
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -1063,6 +1093,7 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
|
||||
export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -1087,6 +1118,7 @@ export type CatalogItemUpdateWithoutLinkedItemsInput = {
|
||||
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -1111,6 +1143,7 @@ export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
|
||||
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
@@ -1174,6 +1207,7 @@ export type CatalogItemCountOutputTypeCountLinkedToArgs<ExtArgs extends runtime.
|
||||
export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
cwCatalogId?: boolean
|
||||
identifier?: boolean
|
||||
name?: boolean
|
||||
description?: boolean
|
||||
customerDescription?: boolean
|
||||
@@ -1200,6 +1234,7 @@ export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalA
|
||||
export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
cwCatalogId?: boolean
|
||||
identifier?: boolean
|
||||
name?: boolean
|
||||
description?: boolean
|
||||
customerDescription?: boolean
|
||||
@@ -1223,6 +1258,7 @@ export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
|
||||
export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
cwCatalogId?: boolean
|
||||
identifier?: boolean
|
||||
name?: boolean
|
||||
description?: boolean
|
||||
customerDescription?: boolean
|
||||
@@ -1246,6 +1282,7 @@ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
|
||||
export type CatalogItemSelectScalar = {
|
||||
id?: boolean
|
||||
cwCatalogId?: boolean
|
||||
identifier?: boolean
|
||||
name?: boolean
|
||||
description?: boolean
|
||||
customerDescription?: boolean
|
||||
@@ -1266,7 +1303,7 @@ export type CatalogItemSelectScalar = {
|
||||
updatedAt?: boolean
|
||||
}
|
||||
|
||||
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "name" | "description" | "customerDescription" | "internalNotes" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
|
||||
export type CatalogItemOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwCatalogId" | "identifier" | "name" | "description" | "customerDescription" | "internalNotes" | "manufacturer" | "manufactureCwId" | "partNumber" | "vendorName" | "vendorSku" | "vendorCwId" | "price" | "cost" | "inactive" | "salesTaxable" | "onHand" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["catalogItem"]>
|
||||
export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
|
||||
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<ExtArgs>
|
||||
@@ -1284,6 +1321,7 @@ export type $CatalogItemPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
cwCatalogId: number
|
||||
identifier: string | null
|
||||
name: string
|
||||
description: string | null
|
||||
customerDescription: string | null
|
||||
@@ -1729,6 +1767,7 @@ export interface Prisma__CatalogItemClient<T, Null = never, ExtArgs extends runt
|
||||
export interface CatalogItemFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
|
||||
readonly identifier: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||
readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||
readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
|
||||
|
||||
@@ -226,6 +226,7 @@ export type CompanyWhereInput = {
|
||||
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
||||
credentials?: Prisma.CredentialListRelationFilter
|
||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||
opportunities?: Prisma.OpportunityListRelationFilter
|
||||
}
|
||||
|
||||
export type CompanyOrderByWithRelationInput = {
|
||||
@@ -237,6 +238,7 @@ export type CompanyOrderByWithRelationInput = {
|
||||
updatedAt?: Prisma.SortOrder
|
||||
credentials?: Prisma.CredentialOrderByRelationAggregateInput
|
||||
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
|
||||
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
|
||||
}
|
||||
|
||||
export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
||||
@@ -251,6 +253,7 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
||||
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
|
||||
credentials?: Prisma.CredentialListRelationFilter
|
||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||
opportunities?: Prisma.OpportunityListRelationFilter
|
||||
}, "id" | "cw_CompanyId" | "cw_Identifier">
|
||||
|
||||
export type CompanyOrderByWithAggregationInput = {
|
||||
@@ -288,6 +291,7 @@ export type CompanyCreateInput = {
|
||||
updatedAt?: Date | string
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedCreateInput = {
|
||||
@@ -299,6 +303,7 @@ export type CompanyUncheckedCreateInput = {
|
||||
updatedAt?: Date | string
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateInput = {
|
||||
@@ -310,6 +315,7 @@ export type CompanyUpdateInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedUpdateInput = {
|
||||
@@ -321,6 +327,7 @@ export type CompanyUncheckedUpdateInput = {
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyCreateManyInput = {
|
||||
@@ -419,6 +426,22 @@ export type IntFieldUpdateOperationsInput = {
|
||||
divide?: number
|
||||
}
|
||||
|
||||
export type CompanyCreateNestedOneWithoutOpportunitiesInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
|
||||
connect?: Prisma.CompanyWhereUniqueInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateOneWithoutOpportunitiesNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutOpportunitiesInput
|
||||
upsert?: Prisma.CompanyUpsertWithoutOpportunitiesInput
|
||||
disconnect?: Prisma.CompanyWhereInput | boolean
|
||||
delete?: Prisma.CompanyWhereInput | boolean
|
||||
connect?: Prisma.CompanyWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutOpportunitiesInput, Prisma.CompanyUpdateWithoutOpportunitiesInput>, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyCreateNestedOneWithoutCredentialsInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
|
||||
@@ -441,6 +464,7 @@ export type CompanyCreateWithoutUnifiSitesInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
||||
@@ -451,6 +475,7 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
|
||||
@@ -477,6 +502,7 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
||||
@@ -487,6 +513,67 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyCreateWithoutOpportunitiesInput = {
|
||||
id?: string
|
||||
name: string
|
||||
cw_CompanyId: number
|
||||
cw_Identifier: string
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedCreateWithoutOpportunitiesInput = {
|
||||
id?: string
|
||||
name: string
|
||||
cw_CompanyId: number
|
||||
cw_Identifier: string
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyCreateOrConnectWithoutOpportunitiesInput = {
|
||||
where: Prisma.CompanyWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyUpsertWithoutOpportunitiesInput = {
|
||||
update: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||
create: Prisma.XOR<Prisma.CompanyCreateWithoutOpportunitiesInput, Prisma.CompanyUncheckedCreateWithoutOpportunitiesInput>
|
||||
where?: Prisma.CompanyWhereInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateToOneWithWhereWithoutOpportunitiesInput = {
|
||||
where?: Prisma.CompanyWhereInput
|
||||
data: Prisma.XOR<Prisma.CompanyUpdateWithoutOpportunitiesInput, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyUpdateWithoutOpportunitiesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedUpdateWithoutOpportunitiesInput = {
|
||||
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
cw_CompanyId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
cw_Identifier?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyCreateWithoutCredentialsInput = {
|
||||
@@ -497,6 +584,7 @@ export type CompanyCreateWithoutCredentialsInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
||||
@@ -507,6 +595,7 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyCreateOrConnectWithoutCredentialsInput = {
|
||||
@@ -533,6 +622,7 @@ export type CompanyUpdateWithoutCredentialsInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
||||
@@ -543,6 +633,7 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
|
||||
@@ -553,11 +644,13 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
||||
export type CompanyCountOutputType = {
|
||||
credentials: number
|
||||
unifiSites: number
|
||||
opportunities: number
|
||||
}
|
||||
|
||||
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
|
||||
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
|
||||
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,6 +677,13 @@ export type CompanyCountOutputTypeCountUnifiSitesArgs<ExtArgs extends runtime.Ty
|
||||
where?: Prisma.UnifiSiteWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* CompanyCountOutputType without action
|
||||
*/
|
||||
export type CompanyCountOutputTypeCountOpportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.OpportunityWhereInput
|
||||
}
|
||||
|
||||
|
||||
export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||
id?: boolean
|
||||
@@ -594,6 +694,7 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
updatedAt?: boolean
|
||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}, ExtArgs["result"]["company"]>
|
||||
|
||||
@@ -628,6 +729,7 @@ export type CompanyOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||
@@ -638,6 +740,7 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
||||
objects: {
|
||||
credentials: Prisma.$CredentialPayload<ExtArgs>[]
|
||||
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
|
||||
opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
|
||||
}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: string
|
||||
@@ -1042,6 +1145,7 @@ export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
opportunities<T extends Prisma.Company$opportunitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$opportunitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OpportunityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
@@ -1512,6 +1616,30 @@ export type Company$unifiSitesArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
||||
distinct?: Prisma.UnifiSiteScalarFieldEnum | Prisma.UnifiSiteScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Company.opportunities
|
||||
*/
|
||||
export type Company$opportunitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the Opportunity
|
||||
*/
|
||||
select?: Prisma.OpportunitySelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the Opportunity
|
||||
*/
|
||||
omit?: Prisma.OpportunityOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.OpportunityInclude<ExtArgs> | null
|
||||
where?: Prisma.OpportunityWhereInput
|
||||
orderBy?: Prisma.OpportunityOrderByWithRelationInput | Prisma.OpportunityOrderByWithRelationInput[]
|
||||
cursor?: Prisma.OpportunityWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.OpportunityScalarFieldEnum | Prisma.OpportunityScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Company without action
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Opportunity" (
|
||||
"id" TEXT NOT NULL,
|
||||
"cwOpportunityId" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"notes" TEXT,
|
||||
"typeName" TEXT,
|
||||
"typeCwId" INTEGER,
|
||||
"stageName" TEXT,
|
||||
"stageCwId" INTEGER,
|
||||
"statusName" TEXT,
|
||||
"statusCwId" INTEGER,
|
||||
"priorityName" TEXT,
|
||||
"priorityCwId" INTEGER,
|
||||
"ratingName" TEXT,
|
||||
"ratingCwId" INTEGER,
|
||||
"source" TEXT,
|
||||
"campaignName" TEXT,
|
||||
"campaignCwId" INTEGER,
|
||||
"primarySalesRepName" TEXT,
|
||||
"primarySalesRepIdentifier" TEXT,
|
||||
"primarySalesRepCwId" INTEGER,
|
||||
"secondarySalesRepName" TEXT,
|
||||
"secondarySalesRepIdentifier" TEXT,
|
||||
"secondarySalesRepCwId" INTEGER,
|
||||
"companyCwId" INTEGER,
|
||||
"companyName" TEXT,
|
||||
"contactCwId" INTEGER,
|
||||
"contactName" TEXT,
|
||||
"siteCwId" INTEGER,
|
||||
"siteName" TEXT,
|
||||
"customerPO" TEXT,
|
||||
"totalSalesTax" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"locationName" TEXT,
|
||||
"locationCwId" INTEGER,
|
||||
"departmentName" TEXT,
|
||||
"departmentCwId" INTEGER,
|
||||
"expectedCloseDate" TIMESTAMP(3),
|
||||
"pipelineChangeDate" TIMESTAMP(3),
|
||||
"dateBecameLead" TIMESTAMP(3),
|
||||
"closedDate" TIMESTAMP(3),
|
||||
"closedFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"closedByName" TEXT,
|
||||
"closedByCwId" INTEGER,
|
||||
"companyId" TEXT,
|
||||
"cwLastUpdated" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Opportunity_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Opportunity_cwOpportunityId_key" ON "Opportunity"("cwOpportunityId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -77,6 +77,7 @@ model Company {
|
||||
|
||||
credentials Credential[]
|
||||
unifiSites UnifiSite[]
|
||||
opportunities Opportunity[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -85,6 +86,7 @@ model Company {
|
||||
model CatalogItem {
|
||||
id String @id @default(cuid())
|
||||
cwCatalogId Int @unique
|
||||
identifier String? @unique
|
||||
name String
|
||||
description String?
|
||||
customerDescription String?
|
||||
@@ -115,6 +117,73 @@ model CatalogItem {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Opportunity {
|
||||
id String @id @default(cuid())
|
||||
cwOpportunityId Int @unique
|
||||
name String
|
||||
notes String?
|
||||
|
||||
// Stage / status / priority / type / rating stored as JSON references
|
||||
// so we don't need separate lookup tables for CW enums
|
||||
typeName String?
|
||||
typeCwId Int?
|
||||
stageName String?
|
||||
stageCwId Int?
|
||||
statusName String?
|
||||
statusCwId Int?
|
||||
priorityName String?
|
||||
priorityCwId Int?
|
||||
ratingName String?
|
||||
ratingCwId Int?
|
||||
source String?
|
||||
campaignName String?
|
||||
campaignCwId Int?
|
||||
|
||||
// Sales rep references
|
||||
primarySalesRepName String?
|
||||
primarySalesRepIdentifier String?
|
||||
primarySalesRepCwId Int?
|
||||
secondarySalesRepName String?
|
||||
secondarySalesRepIdentifier String?
|
||||
secondarySalesRepCwId Int?
|
||||
|
||||
// Company / contact / site
|
||||
companyCwId Int?
|
||||
companyName String?
|
||||
contactCwId Int?
|
||||
contactName String?
|
||||
siteCwId Int?
|
||||
siteName String?
|
||||
customerPO String?
|
||||
|
||||
// Financials
|
||||
totalSalesTax Float @default(0)
|
||||
|
||||
// Location / department
|
||||
locationName String?
|
||||
locationCwId Int?
|
||||
departmentName String?
|
||||
departmentCwId Int?
|
||||
|
||||
// Dates
|
||||
expectedCloseDate DateTime?
|
||||
pipelineChangeDate DateTime?
|
||||
dateBecameLead DateTime?
|
||||
closedDate DateTime?
|
||||
closedFlag Boolean @default(false)
|
||||
closedByName String?
|
||||
closedByCwId Int?
|
||||
|
||||
// Internal relation to Company (optional, linked by cwCompanyId)
|
||||
companyId String?
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
|
||||
cwLastUpdated DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CredentialType {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/items/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items/:identifier"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const includeLinkedItems = c.req.query("includeLinkedItems") === "true";
|
||||
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item fetched successfully!",
|
||||
item.toJson({ includeLinkedItems }),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/procurement/items/:identifier/linked */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items/:identifier/linked"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
const linkedItems = item.getLinkedItems().map((linked) => linked.toJson());
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Linked catalog items fetched successfully!",
|
||||
linkedItems,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/procurement/items/:identifier/link */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/items/:identifier/link"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
const schema = z.object({ targetId: z.string() }).strict();
|
||||
const { targetId } = schema.parse(body);
|
||||
|
||||
const item = await procurement.linkItems(identifier, targetId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item linked successfully!",
|
||||
item.toJson({ includeLinkedItems: true }),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/items/:identifier/refresh-inventory */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/items/:identifier/refresh-inventory"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
await item.refreshInventory();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Inventory refreshed successfully!",
|
||||
item.toJson(),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.inventory.refresh"] }),
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/procurement/items/:identifier/unlink */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/items/:identifier/unlink"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
const schema = z.object({ targetId: z.string() }).strict();
|
||||
const { targetId } = schema.parse(body);
|
||||
|
||||
const item = await procurement.unlinkItems(identifier, targetId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item unlinked successfully!",
|
||||
item.toJson({ includeLinkedItems: true }),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../managers/procurement";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/count */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/count"],
|
||||
async (c) => {
|
||||
const activeOnly = c.req.query("activeOnly") === "true";
|
||||
|
||||
const count = await procurement.count({ activeOnly });
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item count fetched successfully!",
|
||||
{ count },
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../managers/procurement";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/items */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items"],
|
||||
async (c) => {
|
||||
const page = Number(c.req.query("page") ?? 1);
|
||||
const rpp = Number(c.req.query("rpp") ?? 30);
|
||||
const search = c.req.query("search") as string;
|
||||
const includeInactive = c.req.query("includeInactive") === "true";
|
||||
|
||||
const data = search
|
||||
? await procurement.search(search, page, rpp, { includeInactive })
|
||||
: await procurement.fetchPages(page, rpp, { includeInactive });
|
||||
|
||||
const totalRecords = await procurement.count({
|
||||
activeOnly: !includeInactive,
|
||||
});
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog items fetched successfully!",
|
||||
data.map((item) => item.toJson()),
|
||||
{
|
||||
pagination: {
|
||||
previousPage: page <= 1 ? null : page - 1,
|
||||
currentPage: page,
|
||||
nextPage: page >= totalRecords / rpp ? null : page + 1,
|
||||
totalPages: Math.ceil(totalRecords / rpp),
|
||||
totalRecords,
|
||||
listedRecords: rpp,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as fetch } from "./[id]/fetch";
|
||||
import { default as refreshInventory } from "./[id]/refreshInventory";
|
||||
import { default as link } from "./[id]/link";
|
||||
import { default as unlink } from "./[id]/unlink";
|
||||
import { default as fetchLinked } from "./[id]/fetchLinked";
|
||||
import { default as count } from "./count";
|
||||
|
||||
export { count, fetch, fetchAll, fetchLinked, link, refreshInventory, unlink };
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as procurementRoutes from "../procurement";
|
||||
|
||||
const procurementRouter = new Hono();
|
||||
Object.values(procurementRoutes).map((r) => procurementRouter.route("/", r));
|
||||
|
||||
export default procurementRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as salesRoutes from "../sales";
|
||||
|
||||
const salesRouter = new Hono();
|
||||
Object.values(salesRoutes).map((r) => salesRouter.route("/", r));
|
||||
|
||||
export default salesRouter;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { opportunityCw } from "../../../modules/cw-utils/opportunities/opportunities";
|
||||
|
||||
/* GET /v1/sales/opportunities/:identifier/contacts */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/:identifier/contacts"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
const contacts = await opportunityCw.fetchContacts(item.cwOpportunityId);
|
||||
|
||||
const data = contacts.map((ct) => ({
|
||||
id: ct.id,
|
||||
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,
|
||||
}
|
||||
: null,
|
||||
role: ct.role ? { id: ct.role.id, name: ct.role.name } : null,
|
||||
notes: ct.notes,
|
||||
referralFlag: ct.referralFlag,
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity contacts fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/sales/opportunities/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/:identifier"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity fetched successfully!",
|
||||
item.toJson(),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { opportunityCw } from "../../../modules/cw-utils/opportunities/opportunities";
|
||||
|
||||
/* GET /v1/sales/opportunities/:identifier/forecasts */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/:identifier/forecasts"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
const forecasts = await opportunityCw.fetchForecasts(item.cwOpportunityId);
|
||||
|
||||
const data = forecasts.map((f) => ({
|
||||
id: f.id,
|
||||
forecastType: f.forecastType,
|
||||
forecastMonth: f.forecastMonth,
|
||||
revenue: f.revenue,
|
||||
cost: f.cost,
|
||||
forecastPercentage: f.forecastPercentage,
|
||||
status: f.status ? { id: f.status.id, name: f.status.name } : null,
|
||||
includedFlag: f.includedFlag,
|
||||
linkedFlag: f.linkedFlag,
|
||||
recurringFlag: f.recurringFlag,
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity forecasts fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { opportunityCw } from "../../../modules/cw-utils/opportunities/opportunities";
|
||||
|
||||
/* GET /v1/sales/opportunities/:identifier/notes */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/:identifier/notes"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
const notes = await opportunityCw.fetchNotes(item.cwOpportunityId);
|
||||
|
||||
const data = notes.map((n) => ({
|
||||
id: n.id,
|
||||
text: n.text,
|
||||
type: n.type ? { id: n.type.id, name: n.type.name } : null,
|
||||
flagged: n.flagged,
|
||||
enteredBy: n.enteredBy,
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity notes fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* POST /v1/sales/opportunities/:identifier/refresh */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/:identifier/refresh"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await opportunities.fetchItem(identifier);
|
||||
|
||||
const refreshed = await item.refreshFromCW();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity refreshed from ConnectWise successfully!",
|
||||
refreshed.toJson(),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.refresh"] }),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../managers/opportunities";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* GET /v1/sales/opportunities/count */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/count"],
|
||||
async (c) => {
|
||||
const openOnly = c.req.query("openOnly") === "true";
|
||||
|
||||
const count = await opportunities.count({ openOnly });
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity count fetched successfully!",
|
||||
{ count },
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../managers/opportunities";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* GET /v1/sales/opportunities */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities"],
|
||||
async (c) => {
|
||||
const page = Number(c.req.query("page") ?? 1);
|
||||
const rpp = Number(c.req.query("rpp") ?? 30);
|
||||
const search = c.req.query("search") as string;
|
||||
const includeClosed = c.req.query("includeClosed") === "true";
|
||||
|
||||
const data = search
|
||||
? await opportunities.search(search, page, rpp, { includeClosed })
|
||||
: await opportunities.fetchPages(page, rpp, { includeClosed });
|
||||
|
||||
const totalRecords = await opportunities.count({
|
||||
openOnly: !includeClosed,
|
||||
});
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunities fetched successfully!",
|
||||
data.map((item) => item.toJson()),
|
||||
{
|
||||
pagination: {
|
||||
previousPage: page <= 1 ? null : page - 1,
|
||||
currentPage: page,
|
||||
nextPage: page >= totalRecords / rpp ? null : page + 1,
|
||||
totalPages: Math.ceil(totalRecords / rpp),
|
||||
totalRecords,
|
||||
listedRecords: rpp,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as count } from "./count";
|
||||
import { default as fetch } from "./[id]/fetch";
|
||||
import { default as refresh } from "./[id]/refresh";
|
||||
import { default as forecasts } from "./[id]/forecasts";
|
||||
import { default as notes } from "./[id]/notes";
|
||||
import { default as contacts } from "./[id]/contacts";
|
||||
|
||||
export { count, fetch, fetchAll, forecasts, notes, contacts, refresh };
|
||||
@@ -55,6 +55,8 @@ v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
|
||||
v1.route("/role", require("./routers/roleRouter").default);
|
||||
v1.route("/permissions", require("./routers/permissionRouter").default);
|
||||
v1.route("/unifi", require("./routers/unifiRouter").default);
|
||||
v1.route("/procurement", require("./routers/procurementRouter").default);
|
||||
v1.route("/sales", require("./routers/salesRouter").default);
|
||||
app.route("/v1", v1);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { CatalogItem } 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";
|
||||
|
||||
/**
|
||||
* Catalog Item Controller
|
||||
*
|
||||
* This class encapsulates a catalog item entity and provides domain methods
|
||||
* for accessing, refreshing, and serializing catalog item data. It bridges
|
||||
* the internal database representation with ConnectWise catalog data.
|
||||
*/
|
||||
export class CatalogItemController {
|
||||
public readonly id: string;
|
||||
public name: string;
|
||||
public description: string | null;
|
||||
public customerDescription: string | null;
|
||||
public internalNotes: string | null;
|
||||
|
||||
public readonly cwCatalogId: number;
|
||||
public readonly identifier: string | null;
|
||||
|
||||
public manufacturer: string | null;
|
||||
public manufactureCwId: number | null;
|
||||
public partNumber: string | null;
|
||||
|
||||
public vendorName: string | null;
|
||||
public vendorSku: string | null;
|
||||
public vendorCwId: number | null;
|
||||
|
||||
public price: number;
|
||||
public cost: number;
|
||||
|
||||
public inactive: boolean;
|
||||
public salesTaxable: boolean;
|
||||
|
||||
public onHand: number;
|
||||
public cwLastUpdated: Date | null;
|
||||
|
||||
private _linkedItems: CatalogItemController[];
|
||||
|
||||
public readonly createdAt: Date;
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(
|
||||
itemData: CatalogItem & {
|
||||
linkedItems?: CatalogItem[];
|
||||
},
|
||||
) {
|
||||
this.id = itemData.id;
|
||||
this.name = itemData.name;
|
||||
this.description = itemData.description;
|
||||
this.customerDescription = itemData.customerDescription;
|
||||
this.internalNotes = itemData.internalNotes;
|
||||
this.cwCatalogId = itemData.cwCatalogId;
|
||||
this.identifier = itemData.identifier;
|
||||
this.manufacturer = itemData.manufacturer;
|
||||
this.manufactureCwId = itemData.manufactureCwId;
|
||||
this.partNumber = itemData.partNumber;
|
||||
this.vendorName = itemData.vendorName;
|
||||
this.vendorSku = itemData.vendorSku;
|
||||
this.vendorCwId = itemData.vendorCwId;
|
||||
this.price = itemData.price;
|
||||
this.cost = itemData.cost;
|
||||
this.inactive = itemData.inactive;
|
||||
this.salesTaxable = itemData.salesTaxable;
|
||||
this.onHand = itemData.onHand;
|
||||
this.cwLastUpdated = itemData.cwLastUpdated;
|
||||
this.createdAt = itemData.createdAt;
|
||||
this.updatedAt = itemData.updatedAt;
|
||||
|
||||
this._linkedItems = (itemData.linkedItems ?? []).map(
|
||||
(linked) => new CatalogItemController(linked),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Inventory
|
||||
*
|
||||
* Fetches the latest on-hand inventory count from ConnectWise
|
||||
* and updates both the controller state and the database.
|
||||
*
|
||||
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||
*/
|
||||
public async refreshInventory(): Promise<CatalogItemController> {
|
||||
const onHand = await catalogCw.fetchInventoryOnHand(this.cwCatalogId);
|
||||
|
||||
if (onHand !== this.onHand) {
|
||||
await prisma.catalogItem.update({
|
||||
where: { id: this.id },
|
||||
data: { onHand },
|
||||
});
|
||||
this.onHand = onHand;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Linked Items
|
||||
*
|
||||
* Returns the linked catalog items as an array of controllers.
|
||||
*
|
||||
* @returns {CatalogItemController[]} - Array of linked item controllers
|
||||
*/
|
||||
public getLinkedItems(): CatalogItemController[] {
|
||||
return this._linkedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Item
|
||||
*
|
||||
* Links another catalog item to this item. The relationship is bidirectional
|
||||
* via the Prisma implicit many-to-many.
|
||||
*
|
||||
* @param targetId - The internal ID of the catalog item to link
|
||||
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||
*/
|
||||
public async linkItem(targetId: string): Promise<CatalogItemController> {
|
||||
if (targetId === this.id) {
|
||||
throw new GenericError({
|
||||
message: "Cannot link a catalog item to itself",
|
||||
name: "InvalidLinkTarget",
|
||||
cause: `Item '${this.id}' cannot be linked to itself`,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const target = await prisma.catalogItem.findFirst({
|
||||
where: { id: targetId },
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new GenericError({
|
||||
message: "Target catalog item not found",
|
||||
name: "CatalogItemNotFound",
|
||||
cause: `No catalog item exists with ID '${targetId}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await prisma.catalogItem.update({
|
||||
where: { id: this.id },
|
||||
data: {
|
||||
linkedItems: { connect: { id: targetId } },
|
||||
},
|
||||
include: { linkedItems: true },
|
||||
});
|
||||
|
||||
this._linkedItems = (updated.linkedItems ?? []).map(
|
||||
(linked) => new CatalogItemController(linked),
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink Item
|
||||
*
|
||||
* Removes the link between this catalog item and another.
|
||||
*
|
||||
* @param targetId - The internal ID of the catalog item to unlink
|
||||
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||
*/
|
||||
public async unlinkItem(targetId: string): Promise<CatalogItemController> {
|
||||
const updated = await prisma.catalogItem.update({
|
||||
where: { id: this.id },
|
||||
data: {
|
||||
linkedItems: { disconnect: { id: targetId } },
|
||||
},
|
||||
include: { linkedItems: true },
|
||||
});
|
||||
|
||||
this._linkedItems = (updated.linkedItems ?? []).map(
|
||||
(linked) => new CatalogItemController(linked),
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* To JSON
|
||||
*
|
||||
* Serializes the catalog item into a safe, API-friendly object.
|
||||
*
|
||||
* @param opts - Options to control output
|
||||
* @returns - A JSON-safe representation of the catalog item
|
||||
*/
|
||||
public toJson(opts?: { includeLinkedItems?: boolean }): Record<string, any> {
|
||||
return {
|
||||
id: this.id,
|
||||
cwCatalogId: this.cwCatalogId,
|
||||
identifier: this.identifier,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
customerDescription: this.customerDescription,
|
||||
internalNotes: this.internalNotes,
|
||||
manufacturer: this.manufacturer,
|
||||
manufactureCwId: this.manufactureCwId,
|
||||
partNumber: this.partNumber,
|
||||
vendorName: this.vendorName,
|
||||
vendorSku: this.vendorSku,
|
||||
vendorCwId: this.vendorCwId,
|
||||
price: this.price,
|
||||
cost: this.cost,
|
||||
inactive: this.inactive,
|
||||
salesTaxable: this.salesTaxable,
|
||||
onHand: this.onHand,
|
||||
cwLastUpdated: this.cwLastUpdated,
|
||||
linkedItems: opts?.includeLinkedItems
|
||||
? this._linkedItems.map((item) => item.toJson())
|
||||
: undefined,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export class CompanyController {
|
||||
public readonly cw_CompanyId: number;
|
||||
public readonly cw_Data?: {
|
||||
company: CWCompany;
|
||||
defaultContact: Contact;
|
||||
defaultContact: Contact | null;
|
||||
allContacts: Contact[];
|
||||
};
|
||||
|
||||
@@ -96,23 +96,25 @@ export class CompanyController {
|
||||
},
|
||||
primaryContact: !opts?.includePrimaryContact
|
||||
? undefined
|
||||
: {
|
||||
firstName: this.cw_Data?.defaultContact.firstName,
|
||||
lastName: this.cw_Data?.defaultContact.lastName,
|
||||
cwId: this.cw_Data?.defaultContact.id,
|
||||
inactive: this.cw_Data?.defaultContact.inactiveFlag,
|
||||
title: this.cw_Data?.defaultContact.title,
|
||||
phone: this.cw_Data?.defaultContact.defaultPhoneNbr,
|
||||
: this.cw_Data?.defaultContact
|
||||
? {
|
||||
firstName: this.cw_Data.defaultContact.firstName,
|
||||
lastName: this.cw_Data.defaultContact.lastName,
|
||||
cwId: this.cw_Data.defaultContact.id,
|
||||
inactive: this.cw_Data.defaultContact.inactiveFlag,
|
||||
title: this.cw_Data.defaultContact.title,
|
||||
phone: this.cw_Data.defaultContact.defaultPhoneNbr,
|
||||
email: (() => {
|
||||
if (!this.cw_Data?.defaultContact.communicationItems)
|
||||
if (!this.cw_Data?.defaultContact?.communicationItems)
|
||||
return null;
|
||||
return (
|
||||
this.cw_Data?.defaultContact.communicationItems.find(
|
||||
this.cw_Data.defaultContact.communicationItems.find(
|
||||
(v) => v.type.name === "Email",
|
||||
)?.value ?? null
|
||||
);
|
||||
})(),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
allContacts: !opts?.includeAllContacts
|
||||
? undefined
|
||||
: this.cw_Data?.allContacts.map((contact) => ({
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Opportunity } from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { fetchOpportunity } from "../modules/cw-utils/opportunities/fetchOpportunity";
|
||||
import { CWOpportunity } from "../modules/cw-utils/opportunities/opportunity.types";
|
||||
|
||||
/**
|
||||
* Opportunity Controller
|
||||
*
|
||||
* Domain model class that encapsulates an Opportunity entity and provides
|
||||
* methods for accessing, refreshing from ConnectWise, and serializing
|
||||
* opportunity data.
|
||||
*/
|
||||
export class OpportunityController {
|
||||
public readonly id: string;
|
||||
public readonly cwOpportunityId: number;
|
||||
public name: string;
|
||||
public notes: string | null;
|
||||
|
||||
public typeName: string | null;
|
||||
public typeCwId: number | null;
|
||||
public stageName: string | null;
|
||||
public stageCwId: number | null;
|
||||
public statusName: string | null;
|
||||
public statusCwId: number | null;
|
||||
public priorityName: string | null;
|
||||
public priorityCwId: number | null;
|
||||
public ratingName: string | null;
|
||||
public ratingCwId: number | null;
|
||||
public source: string | null;
|
||||
public campaignName: string | null;
|
||||
public campaignCwId: number | null;
|
||||
|
||||
public primarySalesRepName: string | null;
|
||||
public primarySalesRepIdentifier: string | null;
|
||||
public primarySalesRepCwId: number | null;
|
||||
public secondarySalesRepName: string | null;
|
||||
public secondarySalesRepIdentifier: string | null;
|
||||
public secondarySalesRepCwId: number | null;
|
||||
|
||||
public companyCwId: number | null;
|
||||
public companyName: string | null;
|
||||
public contactCwId: number | null;
|
||||
public contactName: string | null;
|
||||
public siteCwId: number | null;
|
||||
public siteName: string | null;
|
||||
public customerPO: string | null;
|
||||
|
||||
public totalSalesTax: number;
|
||||
|
||||
public locationName: string | null;
|
||||
public locationCwId: number | null;
|
||||
public departmentName: string | null;
|
||||
public departmentCwId: number | null;
|
||||
|
||||
public expectedCloseDate: Date | null;
|
||||
public pipelineChangeDate: Date | null;
|
||||
public dateBecameLead: Date | null;
|
||||
public closedDate: Date | null;
|
||||
public closedFlag: boolean;
|
||||
public closedByName: string | null;
|
||||
public closedByCwId: number | null;
|
||||
|
||||
public companyId: string | null;
|
||||
public cwLastUpdated: Date | null;
|
||||
|
||||
public readonly createdAt: Date;
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(data: Opportunity) {
|
||||
this.id = data.id;
|
||||
this.cwOpportunityId = data.cwOpportunityId;
|
||||
this.name = data.name;
|
||||
this.notes = data.notes;
|
||||
|
||||
this.typeName = data.typeName;
|
||||
this.typeCwId = data.typeCwId;
|
||||
this.stageName = data.stageName;
|
||||
this.stageCwId = data.stageCwId;
|
||||
this.statusName = data.statusName;
|
||||
this.statusCwId = data.statusCwId;
|
||||
this.priorityName = data.priorityName;
|
||||
this.priorityCwId = data.priorityCwId;
|
||||
this.ratingName = data.ratingName;
|
||||
this.ratingCwId = data.ratingCwId;
|
||||
this.source = data.source;
|
||||
this.campaignName = data.campaignName;
|
||||
this.campaignCwId = data.campaignCwId;
|
||||
|
||||
this.primarySalesRepName = data.primarySalesRepName;
|
||||
this.primarySalesRepIdentifier = data.primarySalesRepIdentifier;
|
||||
this.primarySalesRepCwId = data.primarySalesRepCwId;
|
||||
this.secondarySalesRepName = data.secondarySalesRepName;
|
||||
this.secondarySalesRepIdentifier = data.secondarySalesRepIdentifier;
|
||||
this.secondarySalesRepCwId = data.secondarySalesRepCwId;
|
||||
|
||||
this.companyCwId = data.companyCwId;
|
||||
this.companyName = data.companyName;
|
||||
this.contactCwId = data.contactCwId;
|
||||
this.contactName = data.contactName;
|
||||
this.siteCwId = data.siteCwId;
|
||||
this.siteName = data.siteName;
|
||||
this.customerPO = data.customerPO;
|
||||
|
||||
this.totalSalesTax = data.totalSalesTax;
|
||||
|
||||
this.locationName = data.locationName;
|
||||
this.locationCwId = data.locationCwId;
|
||||
this.departmentName = data.departmentName;
|
||||
this.departmentCwId = data.departmentCwId;
|
||||
|
||||
this.expectedCloseDate = data.expectedCloseDate;
|
||||
this.pipelineChangeDate = data.pipelineChangeDate;
|
||||
this.dateBecameLead = data.dateBecameLead;
|
||||
this.closedDate = data.closedDate;
|
||||
this.closedFlag = data.closedFlag;
|
||||
this.closedByName = data.closedByName;
|
||||
this.closedByCwId = data.closedByCwId;
|
||||
|
||||
this.companyId = data.companyId;
|
||||
this.cwLastUpdated = data.cwLastUpdated;
|
||||
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh from ConnectWise
|
||||
*
|
||||
* Fetches the latest opportunity data from CW and updates
|
||||
* the local database record and controller state.
|
||||
*/
|
||||
public async refreshFromCW(): Promise<OpportunityController> {
|
||||
const cwData = await fetchOpportunity(this.cwOpportunityId);
|
||||
const mapped = OpportunityController.mapCwToDb(cwData);
|
||||
|
||||
const updated = await prisma.opportunity.update({
|
||||
where: { id: this.id },
|
||||
data: mapped,
|
||||
});
|
||||
|
||||
return new OpportunityController(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch raw CW data
|
||||
*
|
||||
* Returns the raw ConnectWise opportunity object without updating the DB.
|
||||
*/
|
||||
public async fetchCwData(): Promise<CWOpportunity> {
|
||||
return fetchOpportunity(this.cwOpportunityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map CW Opportunity → Prisma create/update payload
|
||||
*
|
||||
* Static helper used by both the controller and the refresh sync.
|
||||
*/
|
||||
public static mapCwToDb(item: CWOpportunity) {
|
||||
return {
|
||||
name: item.name,
|
||||
notes: item.notes ?? null,
|
||||
|
||||
typeName: item.type?.name ?? null,
|
||||
typeCwId: item.type?.id ?? null,
|
||||
stageName: item.stage?.name ?? null,
|
||||
stageCwId: item.stage?.id ?? null,
|
||||
statusName: item.status?.name ?? null,
|
||||
statusCwId: item.status?.id ?? null,
|
||||
priorityName: item.priority?.name ?? null,
|
||||
priorityCwId: item.priority?.id ?? null,
|
||||
ratingName: item.rating?.name ?? null,
|
||||
ratingCwId: item.rating?.id ?? null,
|
||||
source: item.source ?? null,
|
||||
campaignName: item.campaign?.name ?? null,
|
||||
campaignCwId: item.campaign?.id ?? null,
|
||||
|
||||
primarySalesRepName: item.primarySalesRep?.name ?? null,
|
||||
primarySalesRepIdentifier: item.primarySalesRep?.identifier ?? null,
|
||||
primarySalesRepCwId: item.primarySalesRep?.id ?? null,
|
||||
secondarySalesRepName: item.secondarySalesRep?.name ?? null,
|
||||
secondarySalesRepIdentifier: item.secondarySalesRep?.identifier ?? null,
|
||||
secondarySalesRepCwId: item.secondarySalesRep?.id ?? null,
|
||||
|
||||
companyCwId: item.company?.id ?? null,
|
||||
companyName: item.company?.name ?? null,
|
||||
contactCwId: item.contact?.id ?? null,
|
||||
contactName: item.contact?.name ?? null,
|
||||
siteCwId: item.site?.id ?? null,
|
||||
siteName: item.site?.name ?? null,
|
||||
customerPO: item.customerPO ?? null,
|
||||
|
||||
totalSalesTax: item.totalSalesTax ?? 0,
|
||||
|
||||
locationName: item.location?.name ?? null,
|
||||
locationCwId: item.location?.id ?? null,
|
||||
departmentName: item.department?.name ?? null,
|
||||
departmentCwId: item.department?.id ?? null,
|
||||
|
||||
expectedCloseDate: item.expectedCloseDate
|
||||
? new Date(item.expectedCloseDate)
|
||||
: null,
|
||||
pipelineChangeDate: item.pipelineChangeDate
|
||||
? new Date(item.pipelineChangeDate)
|
||||
: null,
|
||||
dateBecameLead: item.dateBecameLead
|
||||
? new Date(item.dateBecameLead)
|
||||
: null,
|
||||
closedDate: item.closedDate ? new Date(item.closedDate) : null,
|
||||
closedFlag: item.closedFlag ?? false,
|
||||
closedByName: item.closedBy?.name ?? null,
|
||||
closedByCwId: item.closedBy?.id ?? null,
|
||||
|
||||
cwLastUpdated: item._info?.lastUpdated
|
||||
? new Date(item._info.lastUpdated)
|
||||
: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To JSON
|
||||
*
|
||||
* Serializes the opportunity into a safe, API-friendly object.
|
||||
*/
|
||||
public toJson(): Record<string, any> {
|
||||
return {
|
||||
id: this.id,
|
||||
cwOpportunityId: this.cwOpportunityId,
|
||||
name: this.name,
|
||||
notes: this.notes,
|
||||
type: this.typeCwId ? { id: this.typeCwId, name: this.typeName } : null,
|
||||
stage: this.stageCwId
|
||||
? { id: this.stageCwId, name: this.stageName }
|
||||
: null,
|
||||
status: this.statusCwId
|
||||
? { id: this.statusCwId, name: this.statusName }
|
||||
: null,
|
||||
priority: this.priorityCwId
|
||||
? { id: this.priorityCwId, name: this.priorityName }
|
||||
: null,
|
||||
rating: this.ratingCwId
|
||||
? { id: this.ratingCwId, name: this.ratingName }
|
||||
: null,
|
||||
source: this.source,
|
||||
campaign: this.campaignCwId
|
||||
? { id: this.campaignCwId, name: this.campaignName }
|
||||
: null,
|
||||
primarySalesRep: this.primarySalesRepCwId
|
||||
? {
|
||||
id: this.primarySalesRepCwId,
|
||||
identifier: this.primarySalesRepIdentifier,
|
||||
name: this.primarySalesRepName,
|
||||
}
|
||||
: null,
|
||||
secondarySalesRep: this.secondarySalesRepCwId
|
||||
? {
|
||||
id: this.secondarySalesRepCwId,
|
||||
identifier: this.secondarySalesRepIdentifier,
|
||||
name: this.secondarySalesRepName,
|
||||
}
|
||||
: null,
|
||||
company: this.companyCwId
|
||||
? { id: this.companyCwId, name: this.companyName }
|
||||
: null,
|
||||
contact: this.contactCwId
|
||||
? { id: this.contactCwId, name: this.contactName }
|
||||
: null,
|
||||
site: this.siteCwId ? { id: this.siteCwId, name: this.siteName } : null,
|
||||
customerPO: this.customerPO,
|
||||
totalSalesTax: this.totalSalesTax,
|
||||
location: this.locationCwId
|
||||
? { id: this.locationCwId, name: this.locationName }
|
||||
: null,
|
||||
department: this.departmentCwId
|
||||
? { id: this.departmentCwId, name: this.departmentName }
|
||||
: null,
|
||||
expectedCloseDate: this.expectedCloseDate,
|
||||
pipelineChangeDate: this.pipelineChangeDate,
|
||||
dateBecameLead: this.dateBecameLead,
|
||||
closedDate: this.closedDate,
|
||||
closedFlag: this.closedFlag,
|
||||
closedBy: this.closedByCwId
|
||||
? { id: this.closedByCwId, name: this.closedByName }
|
||||
: null,
|
||||
companyId: this.companyId,
|
||||
cwLastUpdated: this.cwLastUpdated,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,46 @@ export default class UserController {
|
||||
return decoded.permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Role Permissions
|
||||
*
|
||||
* Verifies and decodes a role permissions JWT and returns the permission nodes.
|
||||
* Returns an empty array if verification fails.
|
||||
*
|
||||
* @param role - Role record containing the signed permissions token
|
||||
* @returns {string[]} The role permission nodes
|
||||
*/
|
||||
private _readRolePermissions(role: Role): string[] {
|
||||
try {
|
||||
const decoded = jwt.verify(role.permissions, permissionsPrivateKey, {
|
||||
algorithms: ["RS256"],
|
||||
issuer: "roles",
|
||||
subject: role.id,
|
||||
}) as DecodedPermissionsBlock;
|
||||
|
||||
return decoded.permissions;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read All Permissions
|
||||
*
|
||||
* Aggregates the user's direct permissions and all permissions from their assigned roles
|
||||
* into a single deduplicated array.
|
||||
*
|
||||
* @returns {Promise<string[]>} Combined array of all permission nodes
|
||||
*/
|
||||
public async readAllPermissions(): Promise<string[]> {
|
||||
const directPermissions = this.readPermissions();
|
||||
const rolePermissions = this._roles
|
||||
.map((role) => this._readRolePermissions(role))
|
||||
.flatMap((permissions) => permissions);
|
||||
|
||||
return [...new Set([...directPermissions, ...rolePermissions])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Roles
|
||||
*
|
||||
@@ -262,7 +302,16 @@ export default class UserController {
|
||||
: this._roles.size > 0
|
||||
? this._roles.map((v) => v.moniker)
|
||||
: undefined,
|
||||
permissions: opts?.safeReturn ? undefined : this.readPermissions(),
|
||||
permissions: opts?.safeReturn
|
||||
? undefined
|
||||
: (() => {
|
||||
const directPermissions = this.readPermissions();
|
||||
const rolePermissions = this._roles
|
||||
.map((role) => this._readRolePermissions(role))
|
||||
.flatMap((permissions) => permissions);
|
||||
|
||||
return [...new Set([...directPermissions, ...rolePermissions])];
|
||||
})(),
|
||||
login: opts?.safeReturn ? undefined : this.login,
|
||||
email: opts?.safeReturn ? undefined : this.email,
|
||||
image: this.image,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { unifiSites } from "./managers/unifiSites";
|
||||
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
|
||||
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
|
||||
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
|
||||
import { refreshOpportunities } from "./modules/cw-utils/opportunities/refreshOpportunities";
|
||||
import { events, setupEventDebugger } from "./modules/globalEvents";
|
||||
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
||||
import { RoleController } from "./controllers/RoleController";
|
||||
@@ -65,6 +66,12 @@ setInterval(
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
|
||||
// Refresh opportunities every minute
|
||||
await refreshOpportunities();
|
||||
setInterval(() => {
|
||||
return refreshOpportunities();
|
||||
}, 60 * 1000);
|
||||
|
||||
await unifiSites.syncSites();
|
||||
setInterval(() => {
|
||||
return unifiSites.syncSites();
|
||||
|
||||
@@ -15,16 +15,19 @@ export const companies = {
|
||||
const freshCwData: { data: Company } = await connectWiseApi.get(
|
||||
`/company/companies/${search.cw_CompanyId}`,
|
||||
);
|
||||
const defaultContactData = await connectWiseApi.get(
|
||||
(freshCwData.data as Company).defaultContact._info.contact_href,
|
||||
);
|
||||
|
||||
const contactHref = freshCwData.data.defaultContact?._info?.contact_href;
|
||||
const defaultContactData = contactHref
|
||||
? await connectWiseApi.get(contactHref)
|
||||
: undefined;
|
||||
|
||||
const allContactsData = await connectWiseApi.get(
|
||||
`${freshCwData.data._info.contacts_href}&pageSize=1000`,
|
||||
);
|
||||
|
||||
return new CompanyController(search, {
|
||||
company: freshCwData.data,
|
||||
defaultContact: defaultContactData.data,
|
||||
defaultContact: defaultContactData?.data ?? null,
|
||||
allContacts: allContactsData.data,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { prisma } from "../constants";
|
||||
import { OpportunityController } from "../controllers/OpportunityController";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
export const opportunities = {
|
||||
/**
|
||||
* Fetch Opportunity
|
||||
*
|
||||
* Fetch an opportunity by its internal ID or ConnectWise opportunity ID
|
||||
* and return an OpportunityController instance.
|
||||
*
|
||||
* @param identifier - The internal ID (string) or CW opportunity ID (number)
|
||||
* @returns {Promise<OpportunityController>}
|
||||
*/
|
||||
async fetchItem(identifier: string | number): Promise<OpportunityController> {
|
||||
const isNumeric =
|
||||
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||
|
||||
const item = await prisma.opportunity.findFirst({
|
||||
where: isNumeric
|
||||
? { cwOpportunityId: Number(identifier) }
|
||||
: { id: identifier as string },
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
throw new GenericError({
|
||||
message: "Opportunity not found",
|
||||
name: "OpportunityNotFound",
|
||||
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return new OpportunityController(item);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Opportunities (Paginated)
|
||||
*
|
||||
* @param page - Page number (1-based)
|
||||
* @param rpp - Records per page
|
||||
* @param opts - Optional filters
|
||||
* @returns {Promise<OpportunityController[]>}
|
||||
*/
|
||||
async fetchPages(
|
||||
page: number,
|
||||
rpp: number,
|
||||
opts?: { includeClosed?: boolean },
|
||||
): Promise<OpportunityController[]> {
|
||||
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||
|
||||
const items = await prisma.opportunity.findMany({
|
||||
where: opts?.includeClosed ? undefined : { closedFlag: false },
|
||||
skip,
|
||||
take: rpp,
|
||||
orderBy: { expectedCloseDate: "asc" },
|
||||
});
|
||||
|
||||
return items.map((item) => new OpportunityController(item));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search Opportunities
|
||||
*
|
||||
* Search opportunities by name, company name, contact name, notes,
|
||||
* sales rep, or status with pagination support.
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param page - Page number (1-based)
|
||||
* @param rpp - Records per page
|
||||
* @param opts - Optional filters
|
||||
* @returns {Promise<OpportunityController[]>}
|
||||
*/
|
||||
async search(
|
||||
query: string,
|
||||
page: number,
|
||||
rpp: number,
|
||||
opts?: { includeClosed?: boolean },
|
||||
): Promise<OpportunityController[]> {
|
||||
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||
|
||||
const items = await prisma.opportunity.findMany({
|
||||
where: {
|
||||
...(opts?.includeClosed ? {} : { closedFlag: false }),
|
||||
OR: [
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ companyName: { contains: query, mode: "insensitive" } },
|
||||
{ contactName: { contains: query, mode: "insensitive" } },
|
||||
{ notes: { contains: query, mode: "insensitive" } },
|
||||
{ primarySalesRepName: { contains: query, mode: "insensitive" } },
|
||||
{ statusName: { contains: query, mode: "insensitive" } },
|
||||
{ stageName: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
skip,
|
||||
take: rpp,
|
||||
orderBy: { expectedCloseDate: "asc" },
|
||||
});
|
||||
|
||||
return items.map((item) => new OpportunityController(item));
|
||||
},
|
||||
|
||||
/**
|
||||
* Count Opportunities
|
||||
*
|
||||
* @param opts - Optional filters
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async count(opts?: { openOnly?: boolean }): Promise<number> {
|
||||
return prisma.opportunity.count({
|
||||
where: opts?.openOnly ? { closedFlag: false } : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Opportunities by Company
|
||||
*
|
||||
* Fetch all opportunities for a company by its internal company ID.
|
||||
*
|
||||
* @param companyId - The internal company ID
|
||||
* @param opts - Optional filters
|
||||
* @returns {Promise<OpportunityController[]>}
|
||||
*/
|
||||
async fetchByCompany(
|
||||
companyId: string,
|
||||
opts?: { includeClosed?: boolean },
|
||||
): Promise<OpportunityController[]> {
|
||||
const items = await prisma.opportunity.findMany({
|
||||
where: {
|
||||
companyId,
|
||||
...(opts?.includeClosed ? {} : { closedFlag: false }),
|
||||
},
|
||||
orderBy: { expectedCloseDate: "asc" },
|
||||
});
|
||||
|
||||
return items.map((item) => new OpportunityController(item));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import { prisma } from "../constants";
|
||||
import { CatalogItemController } from "../controllers/CatalogItemController";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
/**
|
||||
* Standard include clause used by catalog item queries.
|
||||
* Includes one level of linked items.
|
||||
*/
|
||||
const catalogItemInclude = {
|
||||
linkedItems: true,
|
||||
} as const;
|
||||
|
||||
export const procurement = {
|
||||
/**
|
||||
* Fetch Catalog Item
|
||||
*
|
||||
* Fetch a catalog item by its internal ID or ConnectWise catalog ID
|
||||
* and return a CatalogItemController instance.
|
||||
*
|
||||
* @param identifier - The internal ID (string) or CW catalog ID (number)
|
||||
* @returns {Promise<CatalogItemController>} - The catalog item controller
|
||||
*/
|
||||
async fetchItem(identifier: string | number): Promise<CatalogItemController> {
|
||||
const isNumeric =
|
||||
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||
|
||||
const item = await prisma.catalogItem.findFirst({
|
||||
where: isNumeric
|
||||
? { cwCatalogId: Number(identifier) }
|
||||
: {
|
||||
OR: [
|
||||
{ id: identifier as string },
|
||||
{ identifier: identifier as string },
|
||||
],
|
||||
},
|
||||
include: catalogItemInclude,
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
throw new GenericError({
|
||||
message: "Catalog item not found",
|
||||
name: "CatalogItemNotFound",
|
||||
cause: `No catalog item exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return new CatalogItemController(item);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Catalog Items (Paginated)
|
||||
*
|
||||
* Fetch pages of catalog items for pagination.
|
||||
*
|
||||
* @param page - Page number (1-based)
|
||||
* @param rpp - Records per page
|
||||
* @returns {Promise<CatalogItemController[]>} - Array of catalog item controllers
|
||||
*/
|
||||
async fetchPages(
|
||||
page: number,
|
||||
rpp: number,
|
||||
opts?: { includeInactive?: boolean },
|
||||
): Promise<CatalogItemController[]> {
|
||||
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||
const take = rpp;
|
||||
|
||||
const items = await prisma.catalogItem.findMany({
|
||||
where: opts?.includeInactive ? undefined : { inactive: false },
|
||||
skip,
|
||||
take,
|
||||
include: catalogItemInclude,
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return items.map((item) => new CatalogItemController(item));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search Catalog Items
|
||||
*
|
||||
* Search catalog items by name, description, part number, or vendor SKU
|
||||
* with pagination support.
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param page - Page number (1-based)
|
||||
* @param rpp - Records per page
|
||||
* @returns {Promise<CatalogItemController[]>} - Array of matching catalog item controllers
|
||||
*/
|
||||
async search(
|
||||
query: string,
|
||||
page: number,
|
||||
rpp: number,
|
||||
opts?: { includeInactive?: boolean },
|
||||
): Promise<CatalogItemController[]> {
|
||||
const skip = (Math.max(page, 1) - 1) * rpp;
|
||||
const take = rpp;
|
||||
|
||||
const items = await prisma.catalogItem.findMany({
|
||||
where: {
|
||||
...(opts?.includeInactive ? {} : { inactive: false }),
|
||||
OR: [
|
||||
{ identifier: { contains: query, mode: "insensitive" } },
|
||||
{ name: { contains: query, mode: "insensitive" } },
|
||||
{ description: { contains: query, mode: "insensitive" } },
|
||||
{ partNumber: { contains: query, mode: "insensitive" } },
|
||||
{ vendorSku: { contains: query, mode: "insensitive" } },
|
||||
{ manufacturer: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
include: catalogItemInclude,
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return items.map((item) => new CatalogItemController(item));
|
||||
},
|
||||
|
||||
/**
|
||||
* Count Catalog Items
|
||||
*
|
||||
* Returns the total number of catalog items in the database.
|
||||
*
|
||||
* @param opts - Optional filters
|
||||
* @returns {Promise<number>} - Total count
|
||||
*/
|
||||
async count(opts?: { activeOnly?: boolean }): Promise<number> {
|
||||
return prisma.catalogItem.count({
|
||||
where: opts?.activeOnly ? { inactive: false } : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Link Catalog Items
|
||||
*
|
||||
* Links a target catalog item to a source catalog item.
|
||||
*
|
||||
* @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
|
||||
* @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
|
||||
* @returns {Promise<CatalogItemController>} - The updated source controller with linked items
|
||||
*/
|
||||
async linkItems(
|
||||
sourceIdentifier: string | number,
|
||||
targetIdentifier: string | number,
|
||||
): Promise<CatalogItemController> {
|
||||
const source = await procurement.fetchItem(sourceIdentifier);
|
||||
const target = await procurement.fetchItem(targetIdentifier);
|
||||
|
||||
return source.linkItem(target.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unlink Catalog Items
|
||||
*
|
||||
* Removes the link between a source catalog item and a target catalog item.
|
||||
*
|
||||
* @param sourceIdentifier - The source item's internal ID, identifier, or CW catalog ID
|
||||
* @param targetIdentifier - The target item's internal ID, identifier, or CW catalog ID
|
||||
* @returns {Promise<CatalogItemController>} - The updated source controller
|
||||
*/
|
||||
async unlinkItems(
|
||||
sourceIdentifier: string | number,
|
||||
targetIdentifier: string | number,
|
||||
): Promise<CatalogItemController> {
|
||||
const source = await procurement.fetchItem(sourceIdentifier);
|
||||
const target = await procurement.fetchItem(targetIdentifier);
|
||||
|
||||
return source.unlinkItem(target.id);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { opportunityCw } from "./opportunities";
|
||||
import { CWOpportunity } from "./opportunity.types";
|
||||
|
||||
/**
|
||||
* Fetch all opportunities from ConnectWise with optional conditions.
|
||||
*
|
||||
* @param conditions - Optional CW conditions string for filtering
|
||||
* @returns A Collection of CW opportunities keyed by their ID
|
||||
* @throws GenericError if the fetch fails
|
||||
*/
|
||||
export const fetchAllOpportunities = async (
|
||||
conditions?: string,
|
||||
): Promise<Collection<number, CWOpportunity>> => {
|
||||
try {
|
||||
return await opportunityCw.fetchAll(conditions);
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
console.error("Error fetching all opportunities:", errBody);
|
||||
throw new GenericError({
|
||||
name: "FetchAllOpportunitiesError",
|
||||
message: "Failed to fetch opportunities from ConnectWise",
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { opportunityCw } from "./opportunities";
|
||||
import { CWOpportunity } from "./opportunity.types";
|
||||
|
||||
/**
|
||||
* Fetch all opportunities for a specific company from ConnectWise.
|
||||
*
|
||||
* @param cwCompanyId - The ConnectWise company ID
|
||||
* @returns A Collection of CW opportunities for the company keyed by their ID
|
||||
* @throws GenericError if the fetch fails
|
||||
*/
|
||||
export const fetchCompanyOpportunities = async (
|
||||
cwCompanyId: number,
|
||||
): Promise<Collection<number, CWOpportunity>> => {
|
||||
try {
|
||||
return await opportunityCw.fetchByCompany(cwCompanyId);
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
console.error(
|
||||
`Error fetching opportunities for company ${cwCompanyId}:`,
|
||||
errBody,
|
||||
);
|
||||
throw new GenericError({
|
||||
name: "FetchCompanyOpportunitiesError",
|
||||
message: `Failed to fetch opportunities for company ${cwCompanyId}`,
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { opportunityCw } from "./opportunities";
|
||||
import { CWOpportunity } from "./opportunity.types";
|
||||
|
||||
/**
|
||||
* Fetch a single opportunity by its ConnectWise ID.
|
||||
*
|
||||
* @param cwOpportunityId - The ConnectWise opportunity ID
|
||||
* @returns The full CW opportunity object
|
||||
* @throws GenericError if the fetch fails
|
||||
*/
|
||||
export const fetchOpportunity = async (
|
||||
cwOpportunityId: number,
|
||||
): Promise<CWOpportunity> => {
|
||||
try {
|
||||
return await opportunityCw.fetch(cwOpportunityId);
|
||||
} catch (error) {
|
||||
const errBody = (error as any).response?.data || error;
|
||||
console.error(
|
||||
`Error fetching opportunity with ID ${cwOpportunityId}:`,
|
||||
errBody,
|
||||
);
|
||||
throw new GenericError({
|
||||
name: "FetchOpportunityError",
|
||||
message: `Failed to fetch opportunity ${cwOpportunityId}`,
|
||||
cause: typeof errBody === "string" ? errBody : JSON.stringify(errBody),
|
||||
status: 502,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import { connectWiseApi } from "../../../constants";
|
||||
import {
|
||||
CWOpportunity,
|
||||
CWOpportunitySummary,
|
||||
CWForecastItem,
|
||||
CWOpportunityNote,
|
||||
CWOpportunityContact,
|
||||
} from "./opportunity.types";
|
||||
|
||||
export const opportunityCw = {
|
||||
/**
|
||||
* Count Opportunities
|
||||
*
|
||||
* Returns the total number of opportunities in ConnectWise.
|
||||
* Optionally accepts CW conditions string for filtered counts.
|
||||
*/
|
||||
countItems: async (conditions?: string): Promise<number> => {
|
||||
const query = conditions
|
||||
? `/sales/opportunities/count?conditions=${encodeURIComponent(conditions)}`
|
||||
: "/sales/opportunities/count";
|
||||
const response = await connectWiseApi.get(query);
|
||||
return response.data.count;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Opportunity Summaries
|
||||
*
|
||||
* Lightweight fetch returning only id and _info (for lastUpdated comparison).
|
||||
* Paginates through all opportunities.
|
||||
*/
|
||||
fetchAllSummaries: async (): Promise<
|
||||
Collection<number, CWOpportunitySummary>
|
||||
> => {
|
||||
const allItems = new Collection<number, CWOpportunitySummary>();
|
||||
const pageSize = 1000;
|
||||
|
||||
const count = await opportunityCw.countItems();
|
||||
const totalPages = Math.ceil(count / pageSize);
|
||||
|
||||
for (let page = 0; page < totalPages; page++) {
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/opportunities?page=${page + 1}&pageSize=${pageSize}&fields=id,_info`,
|
||||
);
|
||||
const items: CWOpportunitySummary[] = response.data;
|
||||
|
||||
for (const item of items) {
|
||||
allItems.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Opportunities (Full)
|
||||
*
|
||||
* Fetches all opportunities with complete data. Paginates through
|
||||
* the full list.
|
||||
*/
|
||||
fetchAll: async (
|
||||
conditions?: string,
|
||||
): Promise<Collection<number, CWOpportunity>> => {
|
||||
const allItems = new Collection<number, CWOpportunity>();
|
||||
const pageSize = 1000;
|
||||
|
||||
const count = await opportunityCw.countItems(conditions);
|
||||
const totalPages = Math.ceil(count / pageSize);
|
||||
|
||||
for (let page = 0; page < totalPages; page++) {
|
||||
const conditionsParam = conditions
|
||||
? `&conditions=${encodeURIComponent(conditions)}`
|
||||
: "";
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/opportunities?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`,
|
||||
);
|
||||
const items: CWOpportunity[] = response.data;
|
||||
|
||||
for (const item of items) {
|
||||
allItems.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Single Opportunity
|
||||
*
|
||||
* Fetches a single opportunity by its ConnectWise ID.
|
||||
*/
|
||||
fetch: async (id: number): Promise<CWOpportunity> => {
|
||||
const response = await connectWiseApi.get(`/sales/opportunities/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Opportunities by Company
|
||||
*
|
||||
* Fetches all opportunities associated with a specific ConnectWise company ID.
|
||||
*/
|
||||
fetchByCompany: async (
|
||||
cwCompanyId: number,
|
||||
): Promise<Collection<number, CWOpportunity>> => {
|
||||
return opportunityCw.fetchAll(`company/id=${cwCompanyId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Opportunity Forecasts
|
||||
*
|
||||
* Fetches forecast/revenue items for a given opportunity.
|
||||
*/
|
||||
fetchForecasts: async (opportunityId: number): Promise<CWForecastItem[]> => {
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/opportunities/${opportunityId}/forecast`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Opportunity Notes
|
||||
*
|
||||
* Fetches notes associated with a given opportunity.
|
||||
*/
|
||||
fetchNotes: async (opportunityId: number): Promise<CWOpportunityNote[]> => {
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/opportunities/${opportunityId}/notes`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Opportunity Contacts
|
||||
*
|
||||
* Fetches contacts associated with a given opportunity.
|
||||
*/
|
||||
fetchContacts: async (
|
||||
opportunityId: number,
|
||||
): Promise<CWOpportunityContact[]> => {
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/opportunities/${opportunityId}/contacts`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
interface CWReference {
|
||||
id: number;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CWMemberReference {
|
||||
id: number;
|
||||
identifier: string;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CWCompanyReference {
|
||||
id: number;
|
||||
identifier: string;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CWContactReference {
|
||||
id: number;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CWSiteReference {
|
||||
id: number;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CWCustomField {
|
||||
id: number;
|
||||
caption: string;
|
||||
type: string;
|
||||
entryMethod: string;
|
||||
numberOfDecimals: number;
|
||||
value: unknown;
|
||||
connectWiseId: string;
|
||||
rowNum: number;
|
||||
userDefinedFieldRecId: number;
|
||||
podId: string;
|
||||
}
|
||||
|
||||
export interface CWOpportunity {
|
||||
id: number;
|
||||
name: string;
|
||||
expectedCloseDate: string;
|
||||
type: CWReference;
|
||||
stage: CWReference;
|
||||
status: CWReference;
|
||||
priority: CWReference;
|
||||
notes: string;
|
||||
source: string;
|
||||
rating: CWReference;
|
||||
campaign: CWReference;
|
||||
primarySalesRep: CWMemberReference;
|
||||
secondarySalesRep: CWMemberReference;
|
||||
locationId: number;
|
||||
businessUnitId: number;
|
||||
company: CWCompanyReference;
|
||||
contact: CWContactReference;
|
||||
site: CWSiteReference;
|
||||
customerPO: string;
|
||||
pipelineChangeDate: string;
|
||||
dateBecameLead: string;
|
||||
closedDate: string;
|
||||
closedBy: CWMemberReference;
|
||||
totalSalesTax: number;
|
||||
shipToCompany: CWCompanyReference;
|
||||
shipToContact: CWContactReference;
|
||||
shipToSite: CWSiteReference;
|
||||
billToCompany: CWCompanyReference;
|
||||
billToContact: CWContactReference;
|
||||
billToSite: CWSiteReference;
|
||||
billingTerms: CWReference;
|
||||
taxCode: CWReference;
|
||||
currency: CWReference;
|
||||
companyLocationId: number;
|
||||
location: CWReference;
|
||||
department: CWReference;
|
||||
closedFlag: boolean;
|
||||
mobileGuid: string;
|
||||
customFields: CWCustomField[];
|
||||
_info: CWOpportunityInfo;
|
||||
}
|
||||
|
||||
export interface CWOpportunityInfo {
|
||||
lastUpdated: string;
|
||||
updatedBy: string;
|
||||
dateEntered: string;
|
||||
enteredBy: string;
|
||||
forecasts_href: string;
|
||||
notes_href: string;
|
||||
products_href: string;
|
||||
contacts_href: string;
|
||||
configurations_href: string;
|
||||
team_href: string;
|
||||
documents_href: string;
|
||||
activities_href: string;
|
||||
}
|
||||
|
||||
export interface CWForecastItem {
|
||||
id: number;
|
||||
opportunity: CWReference;
|
||||
forecastType: string;
|
||||
forecastMonth: string;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
forecastPercentage: number;
|
||||
status: CWReference;
|
||||
includedFlag: boolean;
|
||||
linkedFlag: boolean;
|
||||
recurringFlag: boolean;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CWOpportunityNote {
|
||||
id: number;
|
||||
opportunity: CWReference;
|
||||
text: string;
|
||||
type: CWReference;
|
||||
flagged: boolean;
|
||||
enteredBy: string;
|
||||
mobileGuid: string;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CWOpportunityContact {
|
||||
id: number;
|
||||
opportunity: CWReference;
|
||||
contact: CWContactReference;
|
||||
company: CWCompanyReference;
|
||||
role: CWReference;
|
||||
notes: string;
|
||||
referralFlag: boolean;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CWOpportunitySummary {
|
||||
id: number;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CWOpportunity } from "./opportunity.types";
|
||||
|
||||
export type ProcessedOpportunity = ReturnType<
|
||||
typeof processOpportunityResponse
|
||||
>;
|
||||
|
||||
/**
|
||||
* Processes raw CW opportunity data into a cleaner, normalized shape
|
||||
* suitable for API responses and internal consumption.
|
||||
*/
|
||||
export const processOpportunityResponse = (opportunity: CWOpportunity) => ({
|
||||
id: opportunity.id,
|
||||
name: opportunity.name,
|
||||
expectedCloseDate: opportunity.expectedCloseDate,
|
||||
closedDate: opportunity.closedDate,
|
||||
closedFlag: opportunity.closedFlag,
|
||||
type: opportunity.type
|
||||
? { id: opportunity.type.id, name: opportunity.type.name }
|
||||
: null,
|
||||
stage: opportunity.stage
|
||||
? { id: opportunity.stage.id, name: opportunity.stage.name }
|
||||
: null,
|
||||
status: opportunity.status
|
||||
? { id: opportunity.status.id, name: opportunity.status.name }
|
||||
: null,
|
||||
priority: opportunity.priority
|
||||
? { id: opportunity.priority.id, name: opportunity.priority.name }
|
||||
: null,
|
||||
rating: opportunity.rating
|
||||
? { id: opportunity.rating.id, name: opportunity.rating.name }
|
||||
: null,
|
||||
source: opportunity.source,
|
||||
notes: opportunity.notes,
|
||||
customerPO: opportunity.customerPO,
|
||||
company: opportunity.company
|
||||
? {
|
||||
id: opportunity.company.id,
|
||||
identifier: opportunity.company.identifier,
|
||||
name: opportunity.company.name,
|
||||
}
|
||||
: null,
|
||||
contact: opportunity.contact
|
||||
? { id: opportunity.contact.id, name: opportunity.contact.name }
|
||||
: null,
|
||||
site: opportunity.site
|
||||
? { id: opportunity.site.id, name: opportunity.site.name }
|
||||
: null,
|
||||
primarySalesRep: opportunity.primarySalesRep
|
||||
? {
|
||||
id: opportunity.primarySalesRep.id,
|
||||
identifier: opportunity.primarySalesRep.identifier,
|
||||
name: opportunity.primarySalesRep.name,
|
||||
}
|
||||
: null,
|
||||
secondarySalesRep: opportunity.secondarySalesRep
|
||||
? {
|
||||
id: opportunity.secondarySalesRep.id,
|
||||
identifier: opportunity.secondarySalesRep.identifier,
|
||||
name: opportunity.secondarySalesRep.name,
|
||||
}
|
||||
: null,
|
||||
closedBy: opportunity.closedBy
|
||||
? {
|
||||
id: opportunity.closedBy.id,
|
||||
identifier: opportunity.closedBy.identifier,
|
||||
name: opportunity.closedBy.name,
|
||||
}
|
||||
: null,
|
||||
campaign: opportunity.campaign
|
||||
? { id: opportunity.campaign.id, name: opportunity.campaign.name }
|
||||
: null,
|
||||
totalSalesTax: opportunity.totalSalesTax,
|
||||
location: opportunity.location
|
||||
? { id: opportunity.location.id, name: opportunity.location.name }
|
||||
: null,
|
||||
department: opportunity.department
|
||||
? { id: opportunity.department.id, name: opportunity.department.name }
|
||||
: null,
|
||||
pipelineChangeDate: opportunity.pipelineChangeDate,
|
||||
dateBecameLead: opportunity.dateBecameLead,
|
||||
info: opportunity._info,
|
||||
});
|
||||
|
||||
/**
|
||||
* Processes an array of raw CW opportunities.
|
||||
*/
|
||||
export const processOpportunitiesResponse = (opportunities: CWOpportunity[]) =>
|
||||
opportunities.map(processOpportunityResponse);
|
||||
@@ -0,0 +1,110 @@
|
||||
import { prisma } from "../../../constants";
|
||||
import { events } from "../../globalEvents";
|
||||
import { opportunityCw } from "./opportunities";
|
||||
import { OpportunityController } from "../../../controllers/OpportunityController";
|
||||
|
||||
/**
|
||||
* Refresh Opportunities
|
||||
*
|
||||
* Syncs local opportunity records with ConnectWise using the same
|
||||
* stale-check pattern as refreshCatalog:
|
||||
* 1. Fetch lightweight summaries (id + _info.lastUpdated)
|
||||
* 2. Compare against local cwLastUpdated timestamps
|
||||
* 3. Full-fetch only stale/new records
|
||||
* 4. Upsert stale items, optionally linking to internal Company
|
||||
*/
|
||||
export const refreshOpportunities = async () => {
|
||||
events.emit("cw:opportunities:refresh:check");
|
||||
|
||||
// 1. Fetch lightweight summaries from CW
|
||||
const cwSummaries = await opportunityCw.fetchAllSummaries();
|
||||
|
||||
// 2. Fetch all DB items with their cwOpportunityId and cwLastUpdated
|
||||
const dbItems = await prisma.opportunity.findMany({
|
||||
select: { cwOpportunityId: true, cwLastUpdated: true },
|
||||
});
|
||||
const dbMap = new Map(
|
||||
dbItems.map((item) => [item.cwOpportunityId, item.cwLastUpdated]),
|
||||
);
|
||||
|
||||
// 3. Determine stale / new IDs
|
||||
const staleIds: number[] = [];
|
||||
|
||||
for (const [cwId, summary] of cwSummaries) {
|
||||
const cwLastUpdated = summary._info?.lastUpdated
|
||||
? new Date(summary._info.lastUpdated)
|
||||
: null;
|
||||
const dbLastUpdated = dbMap.get(cwId) ?? null;
|
||||
|
||||
if (!dbLastUpdated || (cwLastUpdated && cwLastUpdated > dbLastUpdated)) {
|
||||
staleIds.push(cwId);
|
||||
}
|
||||
}
|
||||
|
||||
if (staleIds.length === 0) {
|
||||
events.emit("cw:opportunities:refresh:skipped", {
|
||||
totalCw: cwSummaries.size,
|
||||
totalDb: dbItems.length,
|
||||
staleCount: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
events.emit("cw:opportunities:refresh:started", {
|
||||
totalCw: cwSummaries.size,
|
||||
totalDb: dbItems.length,
|
||||
staleCount: staleIds.length,
|
||||
});
|
||||
|
||||
// 4. Full-fetch all opportunities, filter to stale set
|
||||
const staleIdSet = new Set(staleIds);
|
||||
const allCwItems = await opportunityCw.fetchAll();
|
||||
const staleItems = new Map<number, any>();
|
||||
|
||||
for (const [id, item] of allCwItems) {
|
||||
if (staleIdSet.has(id)) {
|
||||
staleItems.set(id, item);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Build a company CW ID → internal ID lookup for linking
|
||||
const companies = await prisma.company.findMany({
|
||||
select: { id: true, cw_CompanyId: true },
|
||||
});
|
||||
const companyMap = new Map(companies.map((c) => [c.cw_CompanyId, c.id]));
|
||||
|
||||
// 6. Upsert stale/new items
|
||||
const updatedCount = (
|
||||
await Promise.all(
|
||||
staleIds.map(async (cwId) => {
|
||||
const item = staleItems.get(cwId);
|
||||
if (!item) return null;
|
||||
|
||||
const mapped = OpportunityController.mapCwToDb(item);
|
||||
const companyId = item.company?.id
|
||||
? (companyMap.get(item.company.id) ?? null)
|
||||
: null;
|
||||
|
||||
return prisma.opportunity.upsert({
|
||||
where: { cwOpportunityId: cwId },
|
||||
create: {
|
||||
cwOpportunityId: cwId,
|
||||
...mapped,
|
||||
companyId,
|
||||
},
|
||||
update: {
|
||||
...mapped,
|
||||
companyId,
|
||||
},
|
||||
});
|
||||
}),
|
||||
)
|
||||
).filter(Boolean).length;
|
||||
|
||||
events.emit("cw:opportunities:refresh:completed", {
|
||||
totalCw: cwSummaries.size,
|
||||
totalDb: dbItems.length,
|
||||
staleCount: staleIds.length,
|
||||
itemsUpdated: updatedCount,
|
||||
});
|
||||
};
|
||||
@@ -91,6 +91,7 @@ export const refreshCatalog = async () => {
|
||||
where: { cwCatalogId: cwId },
|
||||
create: {
|
||||
cwCatalogId: cwId,
|
||||
identifier: item.identifier,
|
||||
name: item.description,
|
||||
description: item.description,
|
||||
customerDescription: item.customerDescription,
|
||||
@@ -110,6 +111,7 @@ export const refreshCatalog = async () => {
|
||||
},
|
||||
update: {
|
||||
name: item.description,
|
||||
identifier: item.identifier,
|
||||
description: item.description,
|
||||
customerDescription: item.customerDescription,
|
||||
internalNotes: item.notes,
|
||||
|
||||
@@ -158,6 +158,25 @@ interface EventTypes {
|
||||
totalItems: number;
|
||||
updatedCount: number;
|
||||
}) => void;
|
||||
|
||||
// ConnectWise Opportunities Events
|
||||
"cw:opportunities:refresh:check": () => void;
|
||||
"cw:opportunities:refresh:started": (data: {
|
||||
totalCw: number;
|
||||
totalDb: number;
|
||||
staleCount: number;
|
||||
}) => void;
|
||||
"cw:opportunities:refresh:completed": (data: {
|
||||
totalCw: number;
|
||||
totalDb: number;
|
||||
staleCount: number;
|
||||
itemsUpdated: number;
|
||||
}) => void;
|
||||
"cw:opportunities:refresh:skipped": (data: {
|
||||
totalCw: number;
|
||||
totalDb: number;
|
||||
staleCount: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const events = new Eventra<EventTypes>();
|
||||
|
||||
@@ -341,6 +341,72 @@ export const PERMISSION_NODES = {
|
||||
],
|
||||
},
|
||||
|
||||
procurement: {
|
||||
name: "Procurement Permissions",
|
||||
description:
|
||||
"Permissions for accessing and managing procurement catalog items",
|
||||
permissions: [
|
||||
{
|
||||
node: "procurement.catalog.fetch",
|
||||
description: "Fetch a single catalog item",
|
||||
usedIn: ["src/api/procurement/[id]/fetch.ts"],
|
||||
},
|
||||
{
|
||||
node: "procurement.catalog.fetch.many",
|
||||
description: "Fetch multiple catalog items or count",
|
||||
usedIn: [
|
||||
"src/api/procurement/fetchAll.ts",
|
||||
"src/api/procurement/count.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
node: "procurement.catalog.inventory.refresh",
|
||||
description:
|
||||
"Refresh on-hand inventory for a catalog item from ConnectWise",
|
||||
usedIn: ["src/api/procurement/[id]/refreshInventory.ts"],
|
||||
dependencies: ["procurement.catalog.fetch"],
|
||||
},
|
||||
{
|
||||
node: "procurement.catalog.link",
|
||||
description: "Link or unlink catalog items to each other",
|
||||
usedIn: [
|
||||
"src/api/procurement/[id]/link.ts",
|
||||
"src/api/procurement/[id]/unlink.ts",
|
||||
],
|
||||
dependencies: ["procurement.catalog.fetch"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
sales: {
|
||||
name: "Sales Permissions",
|
||||
description: "Permissions for accessing and managing sales opportunities",
|
||||
permissions: [
|
||||
{
|
||||
node: "sales.opportunity.fetch",
|
||||
description:
|
||||
"Fetch a single opportunity and its sub-resources (forecasts, notes, contacts)",
|
||||
usedIn: [
|
||||
"src/api/sales/[id]/fetch.ts",
|
||||
"src/api/sales/[id]/forecasts.ts",
|
||||
"src/api/sales/[id]/notes.ts",
|
||||
"src/api/sales/[id]/contacts.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
node: "sales.opportunity.fetch.many",
|
||||
description: "Fetch multiple opportunities or count",
|
||||
usedIn: ["src/api/sales/fetchAll.ts", "src/api/sales/count.ts"],
|
||||
},
|
||||
{
|
||||
node: "sales.opportunity.refresh",
|
||||
description: "Refresh a single opportunity from ConnectWise",
|
||||
usedIn: ["src/api/sales/[id]/refresh.ts"],
|
||||
dependencies: ["sales.opportunity.fetch"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
unifi: {
|
||||
name: "UniFi Permissions",
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user