Fix UserController permission serialization and include current updates

This commit is contained in:
2026-02-27 14:38:22 -06:00
parent 51eb36f4a6
commit b1f6462ac3
50 changed files with 6150 additions and 30 deletions
+673
View File
@@ -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 ## 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. 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.
+19
View File
@@ -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. - **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. - **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 ### UniFi Permissions
Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes. Permissions for accessing and managing UniFi network infrastructure. The `unifi.access` permission is a gate permission required for **all** UniFi routes.
+5
View File
@@ -47,6 +47,11 @@ export type Company = Prisma.CompanyModel
* *
*/ */
export type CatalogItem = Prisma.CatalogItemModel export type CatalogItem = Prisma.CatalogItemModel
/**
* Model Opportunity
*
*/
export type Opportunity = Prisma.OpportunityModel
/** /**
* Model CredentialType * Model CredentialType
* *
+5
View File
@@ -69,6 +69,11 @@ export type Company = Prisma.CompanyModel
* *
*/ */
export type CatalogItem = Prisma.CatalogItemModel export type CatalogItem = Prisma.CatalogItemModel
/**
* Model Opportunity
*
*/
export type Opportunity = Prisma.OpportunityModel
/** /**
* Model CredentialType * Model CredentialType
* *
File diff suppressed because one or more lines are too long
+130 -1
View File
@@ -390,6 +390,7 @@ export const ModelName = {
UnifiSite: 'UnifiSite', UnifiSite: 'UnifiSite',
Company: 'Company', Company: 'Company',
CatalogItem: 'CatalogItem', CatalogItem: 'CatalogItem',
Opportunity: 'Opportunity',
CredentialType: 'CredentialType', CredentialType: 'CredentialType',
SecureValue: 'SecureValue', SecureValue: 'SecureValue',
Credential: 'Credential' Credential: 'Credential'
@@ -408,7 +409,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions omit: GlobalOmitOptions
} }
meta: { meta: {
modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "credentialType" | "secureValue" | "credential" modelProps: "session" | "user" | "role" | "unifiSite" | "company" | "catalogItem" | "opportunity" | "credentialType" | "secureValue" | "credential"
txIsolationLevel: TransactionIsolationLevel txIsolationLevel: TransactionIsolationLevel
} }
model: { 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: { CredentialType: {
payload: Prisma.$CredentialTypePayload<ExtArgs> payload: Prisma.$CredentialTypePayload<ExtArgs>
fields: Prisma.CredentialTypeFieldRefs fields: Prisma.CredentialTypeFieldRefs
@@ -1186,6 +1261,7 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
export const CatalogItemScalarFieldEnum = { export const CatalogItemScalarFieldEnum = {
id: 'id', id: 'id',
cwCatalogId: 'cwCatalogId', cwCatalogId: 'cwCatalogId',
identifier: 'identifier',
name: 'name', name: 'name',
description: 'description', description: 'description',
customerDescription: 'customerDescription', customerDescription: 'customerDescription',
@@ -1209,6 +1285,58 @@ export const CatalogItemScalarFieldEnum = {
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof 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 = { export const CredentialTypeScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
@@ -1473,6 +1601,7 @@ export type GlobalOmitConfig = {
unifiSite?: Prisma.UnifiSiteOmit unifiSite?: Prisma.UnifiSiteOmit
company?: Prisma.CompanyOmit company?: Prisma.CompanyOmit
catalogItem?: Prisma.CatalogItemOmit catalogItem?: Prisma.CatalogItemOmit
opportunity?: Prisma.OpportunityOmit
credentialType?: Prisma.CredentialTypeOmit credentialType?: Prisma.CredentialTypeOmit
secureValue?: Prisma.SecureValueOmit secureValue?: Prisma.SecureValueOmit
credential?: Prisma.CredentialOmit credential?: Prisma.CredentialOmit
@@ -57,6 +57,7 @@ export const ModelName = {
UnifiSite: 'UnifiSite', UnifiSite: 'UnifiSite',
Company: 'Company', Company: 'Company',
CatalogItem: 'CatalogItem', CatalogItem: 'CatalogItem',
Opportunity: 'Opportunity',
CredentialType: 'CredentialType', CredentialType: 'CredentialType',
SecureValue: 'SecureValue', SecureValue: 'SecureValue',
Credential: 'Credential' Credential: 'Credential'
@@ -147,6 +148,7 @@ export type CompanyScalarFieldEnum = (typeof CompanyScalarFieldEnum)[keyof typeo
export const CatalogItemScalarFieldEnum = { export const CatalogItemScalarFieldEnum = {
id: 'id', id: 'id',
cwCatalogId: 'cwCatalogId', cwCatalogId: 'cwCatalogId',
identifier: 'identifier',
name: 'name', name: 'name',
description: 'description', description: 'description',
customerDescription: 'customerDescription', customerDescription: 'customerDescription',
@@ -170,6 +172,58 @@ export const CatalogItemScalarFieldEnum = {
export type CatalogItemScalarFieldEnum = (typeof CatalogItemScalarFieldEnum)[keyof typeof 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 = { export const CredentialTypeScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
+1
View File
@@ -14,6 +14,7 @@ export type * from './models/Role.ts'
export type * from './models/UnifiSite.ts' export type * from './models/UnifiSite.ts'
export type * from './models/Company.ts' export type * from './models/Company.ts'
export type * from './models/CatalogItem.ts' export type * from './models/CatalogItem.ts'
export type * from './models/Opportunity.ts'
export type * from './models/CredentialType.ts' export type * from './models/CredentialType.ts'
export type * from './models/SecureValue.ts' export type * from './models/SecureValue.ts'
export type * from './models/Credential.ts' export type * from './models/Credential.ts'
+41 -2
View File
@@ -47,6 +47,7 @@ export type CatalogItemSumAggregateOutputType = {
export type CatalogItemMinAggregateOutputType = { export type CatalogItemMinAggregateOutputType = {
id: string | null id: string | null
cwCatalogId: number | null cwCatalogId: number | null
identifier: string | null
name: string | null name: string | null
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
@@ -70,6 +71,7 @@ export type CatalogItemMinAggregateOutputType = {
export type CatalogItemMaxAggregateOutputType = { export type CatalogItemMaxAggregateOutputType = {
id: string | null id: string | null
cwCatalogId: number | null cwCatalogId: number | null
identifier: string | null
name: string | null name: string | null
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
@@ -93,6 +95,7 @@ export type CatalogItemMaxAggregateOutputType = {
export type CatalogItemCountAggregateOutputType = { export type CatalogItemCountAggregateOutputType = {
id: number id: number
cwCatalogId: number cwCatalogId: number
identifier: number
name: number name: number
description: number description: number
customerDescription: number customerDescription: number
@@ -136,6 +139,7 @@ export type CatalogItemSumAggregateInputType = {
export type CatalogItemMinAggregateInputType = { export type CatalogItemMinAggregateInputType = {
id?: true id?: true
cwCatalogId?: true cwCatalogId?: true
identifier?: true
name?: true name?: true
description?: true description?: true
customerDescription?: true customerDescription?: true
@@ -159,6 +163,7 @@ export type CatalogItemMinAggregateInputType = {
export type CatalogItemMaxAggregateInputType = { export type CatalogItemMaxAggregateInputType = {
id?: true id?: true
cwCatalogId?: true cwCatalogId?: true
identifier?: true
name?: true name?: true
description?: true description?: true
customerDescription?: true customerDescription?: true
@@ -182,6 +187,7 @@ export type CatalogItemMaxAggregateInputType = {
export type CatalogItemCountAggregateInputType = { export type CatalogItemCountAggregateInputType = {
id?: true id?: true
cwCatalogId?: true cwCatalogId?: true
identifier?: true
name?: true name?: true
description?: true description?: true
customerDescription?: true customerDescription?: true
@@ -292,6 +298,7 @@ export type CatalogItemGroupByArgs<ExtArgs extends runtime.Types.Extensions.Inte
export type CatalogItemGroupByOutputType = { export type CatalogItemGroupByOutputType = {
id: string id: string
cwCatalogId: number cwCatalogId: number
identifier: string | null
name: string name: string
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
@@ -338,6 +345,7 @@ export type CatalogItemWhereInput = {
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[] NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
id?: Prisma.StringFilter<"CatalogItem"> | string id?: Prisma.StringFilter<"CatalogItem"> | string
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
name?: Prisma.StringFilter<"CatalogItem"> | string name?: Prisma.StringFilter<"CatalogItem"> | string
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
@@ -363,6 +371,7 @@ export type CatalogItemWhereInput = {
export type CatalogItemOrderByWithRelationInput = { export type CatalogItemOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder description?: Prisma.SortOrderInput | Prisma.SortOrder
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -388,6 +397,7 @@ export type CatalogItemOrderByWithRelationInput = {
export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
id?: string id?: string
cwCatalogId?: number cwCatalogId?: number
identifier?: string
AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[] AND?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
OR?: Prisma.CatalogItemWhereInput[] OR?: Prisma.CatalogItemWhereInput[]
NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[] NOT?: Prisma.CatalogItemWhereInput | Prisma.CatalogItemWhereInput[]
@@ -411,11 +421,12 @@ export type CatalogItemWhereUniqueInput = Prisma.AtLeast<{
updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string updatedAt?: Prisma.DateTimeFilter<"CatalogItem"> | Date | string
linkedItems?: Prisma.CatalogItemListRelationFilter linkedItems?: Prisma.CatalogItemListRelationFilter
linkedTo?: Prisma.CatalogItemListRelationFilter linkedTo?: Prisma.CatalogItemListRelationFilter
}, "id" | "cwCatalogId"> }, "id" | "cwCatalogId" | "identifier">
export type CatalogItemOrderByWithAggregationInput = { export type CatalogItemOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrderInput | Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrderInput | Prisma.SortOrder description?: Prisma.SortOrderInput | Prisma.SortOrder
customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder customerDescription?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -447,6 +458,7 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[] NOT?: Prisma.CatalogItemScalarWhereWithAggregatesInput | Prisma.CatalogItemScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string id?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number cwCatalogId?: Prisma.IntWithAggregatesFilter<"CatalogItem"> | number
identifier?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string name?: Prisma.StringWithAggregatesFilter<"CatalogItem"> | string
description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableWithAggregatesFilter<"CatalogItem"> | string | null
@@ -470,6 +482,7 @@ export type CatalogItemScalarWhereWithAggregatesInput = {
export type CatalogItemCreateInput = { export type CatalogItemCreateInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -495,6 +508,7 @@ export type CatalogItemCreateInput = {
export type CatalogItemUncheckedCreateInput = { export type CatalogItemUncheckedCreateInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -520,6 +534,7 @@ export type CatalogItemUncheckedCreateInput = {
export type CatalogItemUpdateInput = { export type CatalogItemUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -545,6 +560,7 @@ export type CatalogItemUpdateInput = {
export type CatalogItemUncheckedUpdateInput = { export type CatalogItemUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -570,6 +586,7 @@ export type CatalogItemUncheckedUpdateInput = {
export type CatalogItemCreateManyInput = { export type CatalogItemCreateManyInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -593,6 +610,7 @@ export type CatalogItemCreateManyInput = {
export type CatalogItemUpdateManyMutationInput = { export type CatalogItemUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -616,6 +634,7 @@ export type CatalogItemUpdateManyMutationInput = {
export type CatalogItemUncheckedUpdateManyInput = { export type CatalogItemUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -649,6 +668,7 @@ export type CatalogItemOrderByRelationAggregateInput = {
export type CatalogItemCountOrderByAggregateInput = { export type CatalogItemCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrder description?: Prisma.SortOrder
customerDescription?: Prisma.SortOrder customerDescription?: Prisma.SortOrder
@@ -681,6 +701,7 @@ export type CatalogItemAvgOrderByAggregateInput = {
export type CatalogItemMaxOrderByAggregateInput = { export type CatalogItemMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrder description?: Prisma.SortOrder
customerDescription?: Prisma.SortOrder customerDescription?: Prisma.SortOrder
@@ -704,6 +725,7 @@ export type CatalogItemMaxOrderByAggregateInput = {
export type CatalogItemMinOrderByAggregateInput = { export type CatalogItemMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
cwCatalogId?: Prisma.SortOrder cwCatalogId?: Prisma.SortOrder
identifier?: Prisma.SortOrder
name?: Prisma.SortOrder name?: Prisma.SortOrder
description?: Prisma.SortOrder description?: Prisma.SortOrder
customerDescription?: Prisma.SortOrder customerDescription?: Prisma.SortOrder
@@ -828,6 +850,7 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsNestedInput = {
export type CatalogItemCreateWithoutLinkedToInput = { export type CatalogItemCreateWithoutLinkedToInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -852,6 +875,7 @@ export type CatalogItemCreateWithoutLinkedToInput = {
export type CatalogItemUncheckedCreateWithoutLinkedToInput = { export type CatalogItemUncheckedCreateWithoutLinkedToInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -881,6 +905,7 @@ export type CatalogItemCreateOrConnectWithoutLinkedToInput = {
export type CatalogItemCreateWithoutLinkedItemsInput = { export type CatalogItemCreateWithoutLinkedItemsInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -905,6 +930,7 @@ export type CatalogItemCreateWithoutLinkedItemsInput = {
export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = { export type CatalogItemUncheckedCreateWithoutLinkedItemsInput = {
id?: string id?: string
cwCatalogId: number cwCatalogId: number
identifier?: string | null
name: string name: string
description?: string | null description?: string | null
customerDescription?: string | null customerDescription?: string | null
@@ -953,6 +979,7 @@ export type CatalogItemScalarWhereInput = {
NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[] NOT?: Prisma.CatalogItemScalarWhereInput | Prisma.CatalogItemScalarWhereInput[]
id?: Prisma.StringFilter<"CatalogItem"> | string id?: Prisma.StringFilter<"CatalogItem"> | string
cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number cwCatalogId?: Prisma.IntFilter<"CatalogItem"> | number
identifier?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
name?: Prisma.StringFilter<"CatalogItem"> | string name?: Prisma.StringFilter<"CatalogItem"> | string
description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null description?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null customerDescription?: Prisma.StringNullableFilter<"CatalogItem"> | string | null
@@ -992,6 +1019,7 @@ export type CatalogItemUpdateManyWithWhereWithoutLinkedItemsInput = {
export type CatalogItemUpdateWithoutLinkedToInput = { export type CatalogItemUpdateWithoutLinkedToInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1016,6 +1044,7 @@ export type CatalogItemUpdateWithoutLinkedToInput = {
export type CatalogItemUncheckedUpdateWithoutLinkedToInput = { export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1040,6 +1069,7 @@ export type CatalogItemUncheckedUpdateWithoutLinkedToInput = {
export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = { export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1063,6 +1093,7 @@ export type CatalogItemUncheckedUpdateManyWithoutLinkedToInput = {
export type CatalogItemUpdateWithoutLinkedItemsInput = { export type CatalogItemUpdateWithoutLinkedItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1087,6 +1118,7 @@ export type CatalogItemUpdateWithoutLinkedItemsInput = {
export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = { export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null customerDescription?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
@@ -1111,6 +1143,7 @@ export type CatalogItemUncheckedUpdateWithoutLinkedItemsInput = {
export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = { export type CatalogItemUncheckedUpdateManyWithoutLinkedItemsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string id?: Prisma.StringFieldUpdateOperationsInput | string
cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number cwCatalogId?: Prisma.IntFieldUpdateOperationsInput | number
identifier?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string name?: Prisma.StringFieldUpdateOperationsInput | string
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerDescription?: 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<{ export type CatalogItemSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: 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<{ export type CatalogItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: 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<{ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: boolean customerDescription?: boolean
@@ -1246,6 +1282,7 @@ export type CatalogItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
export type CatalogItemSelectScalar = { export type CatalogItemSelectScalar = {
id?: boolean id?: boolean
cwCatalogId?: boolean cwCatalogId?: boolean
identifier?: boolean
name?: boolean name?: boolean
description?: boolean description?: boolean
customerDescription?: boolean customerDescription?: boolean
@@ -1266,7 +1303,7 @@ export type CatalogItemSelectScalar = {
updatedAt?: boolean 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> = { export type CatalogItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs> linkedItems?: boolean | Prisma.CatalogItem$linkedItemsArgs<ExtArgs>
linkedTo?: boolean | Prisma.CatalogItem$linkedToArgs<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<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
cwCatalogId: number cwCatalogId: number
identifier: string | null
name: string name: string
description: string | null description: string | null
customerDescription: string | null customerDescription: string | null
@@ -1729,6 +1767,7 @@ export interface Prisma__CatalogItemClient<T, Null = never, ExtArgs extends runt
export interface CatalogItemFieldRefs { export interface CatalogItemFieldRefs {
readonly id: Prisma.FieldRef<"CatalogItem", 'String'> readonly id: Prisma.FieldRef<"CatalogItem", 'String'>
readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'> readonly cwCatalogId: Prisma.FieldRef<"CatalogItem", 'Int'>
readonly identifier: Prisma.FieldRef<"CatalogItem", 'String'>
readonly name: Prisma.FieldRef<"CatalogItem", 'String'> readonly name: Prisma.FieldRef<"CatalogItem", 'String'>
readonly description: Prisma.FieldRef<"CatalogItem", 'String'> readonly description: Prisma.FieldRef<"CatalogItem", 'String'>
readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'> readonly customerDescription: Prisma.FieldRef<"CatalogItem", 'String'>
+128
View File
@@ -226,6 +226,7 @@ export type CompanyWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
credentials?: Prisma.CredentialListRelationFilter credentials?: Prisma.CredentialListRelationFilter
unifiSites?: Prisma.UnifiSiteListRelationFilter unifiSites?: Prisma.UnifiSiteListRelationFilter
opportunities?: Prisma.OpportunityListRelationFilter
} }
export type CompanyOrderByWithRelationInput = { export type CompanyOrderByWithRelationInput = {
@@ -237,6 +238,7 @@ export type CompanyOrderByWithRelationInput = {
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
credentials?: Prisma.CredentialOrderByRelationAggregateInput credentials?: Prisma.CredentialOrderByRelationAggregateInput
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
} }
export type CompanyWhereUniqueInput = Prisma.AtLeast<{ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
@@ -251,6 +253,7 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Company"> | Date | string
credentials?: Prisma.CredentialListRelationFilter credentials?: Prisma.CredentialListRelationFilter
unifiSites?: Prisma.UnifiSiteListRelationFilter unifiSites?: Prisma.UnifiSiteListRelationFilter
opportunities?: Prisma.OpportunityListRelationFilter
}, "id" | "cw_CompanyId" | "cw_Identifier"> }, "id" | "cw_CompanyId" | "cw_Identifier">
export type CompanyOrderByWithAggregationInput = { export type CompanyOrderByWithAggregationInput = {
@@ -288,6 +291,7 @@ export type CompanyCreateInput = {
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
} }
export type CompanyUncheckedCreateInput = { export type CompanyUncheckedCreateInput = {
@@ -299,6 +303,7 @@ export type CompanyUncheckedCreateInput = {
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
} }
export type CompanyUpdateInput = { export type CompanyUpdateInput = {
@@ -310,6 +315,7 @@ export type CompanyUpdateInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
} }
export type CompanyUncheckedUpdateInput = { export type CompanyUncheckedUpdateInput = {
@@ -321,6 +327,7 @@ export type CompanyUncheckedUpdateInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
} }
export type CompanyCreateManyInput = { export type CompanyCreateManyInput = {
@@ -419,6 +426,22 @@ export type IntFieldUpdateOperationsInput = {
divide?: number 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 = { export type CompanyCreateNestedOneWithoutCredentialsInput = {
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput> create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
@@ -441,6 +464,7 @@ export type CompanyCreateWithoutUnifiSitesInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
} }
export type CompanyUncheckedCreateWithoutUnifiSitesInput = { export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
@@ -451,6 +475,7 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
} }
export type CompanyCreateOrConnectWithoutUnifiSitesInput = { export type CompanyCreateOrConnectWithoutUnifiSitesInput = {
@@ -477,6 +502,7 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
} }
export type CompanyUncheckedUpdateWithoutUnifiSitesInput = { export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
@@ -487,6 +513,67 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput 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 = { export type CompanyCreateWithoutCredentialsInput = {
@@ -497,6 +584,7 @@ export type CompanyCreateWithoutCredentialsInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
} }
export type CompanyUncheckedCreateWithoutCredentialsInput = { export type CompanyUncheckedCreateWithoutCredentialsInput = {
@@ -507,6 +595,7 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
} }
export type CompanyCreateOrConnectWithoutCredentialsInput = { export type CompanyCreateOrConnectWithoutCredentialsInput = {
@@ -533,6 +622,7 @@ export type CompanyUpdateWithoutCredentialsInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
} }
export type CompanyUncheckedUpdateWithoutCredentialsInput = { export type CompanyUncheckedUpdateWithoutCredentialsInput = {
@@ -543,6 +633,7 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
} }
@@ -553,11 +644,13 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
export type CompanyCountOutputType = { export type CompanyCountOutputType = {
credentials: number credentials: number
unifiSites: number unifiSites: number
opportunities: number
} }
export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
} }
/** /**
@@ -584,6 +677,13 @@ export type CompanyCountOutputTypeCountUnifiSitesArgs<ExtArgs extends runtime.Ty
where?: Prisma.UnifiSiteWhereInput 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<{ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
@@ -594,6 +694,7 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
updatedAt?: boolean updatedAt?: boolean
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs> credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs> unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["company"]> }, 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> = { export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs> credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs> unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
_count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs> _count?: boolean | Prisma.CompanyCountOutputTypeDefaultArgs<ExtArgs>
} }
export type CompanyIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {} 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: { objects: {
credentials: Prisma.$CredentialPayload<ExtArgs>[] credentials: Prisma.$CredentialPayload<ExtArgs>[]
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[] unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
} }
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string id: string
@@ -1042,6 +1145,7 @@ export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.
readonly [Symbol.toStringTag]: "PrismaPromise" 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> 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> 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. * Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved. * @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[] 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 * 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;
+71 -2
View File
@@ -75,8 +75,9 @@ model Company {
cw_CompanyId Int @unique cw_CompanyId Int @unique
cw_Identifier String @unique cw_Identifier String @unique
credentials Credential[] credentials Credential[]
unifiSites UnifiSite[] unifiSites UnifiSite[]
opportunities Opportunity[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -85,6 +86,7 @@ model Company {
model CatalogItem { model CatalogItem {
id String @id @default(cuid()) id String @id @default(cuid())
cwCatalogId Int @unique cwCatalogId Int @unique
identifier String? @unique
name String name String
description String? description String?
customerDescription String? customerDescription String?
@@ -115,6 +117,73 @@ model CatalogItem {
updatedAt DateTime @updatedAt 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 { model CredentialType {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String @unique
+25
View File
@@ -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"] }),
);
+25
View File
@@ -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"] }),
);
+28
View File
@@ -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"] }),
);
+28
View File
@@ -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"] }),
);
+24
View File
@@ -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"] }),
);
+43
View File
@@ -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"] }),
);
+9
View File
@@ -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 };
+7
View File
@@ -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;
+7
View File
@@ -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;
+41
View File
@@ -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"] }),
);
+24
View File
@@ -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"] }),
);
+39
View File
@@ -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"] }),
);
+34
View File
@@ -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"] }),
);
+25
View File
@@ -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"] }),
);
+24
View File
@@ -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"] }),
);
+43
View File
@@ -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"] }),
);
+9
View File
@@ -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 };
+2
View File
@@ -55,6 +55,8 @@ v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
v1.route("/role", require("./routers/roleRouter").default); v1.route("/role", require("./routers/roleRouter").default);
v1.route("/permissions", require("./routers/permissionRouter").default); v1.route("/permissions", require("./routers/permissionRouter").default);
v1.route("/unifi", require("./routers/unifiRouter").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); app.route("/v1", v1);
export default app; export default app;
+218
View File
@@ -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,
};
}
}
+20 -18
View File
@@ -18,7 +18,7 @@ export class CompanyController {
public readonly cw_CompanyId: number; public readonly cw_CompanyId: number;
public readonly cw_Data?: { public readonly cw_Data?: {
company: CWCompany; company: CWCompany;
defaultContact: Contact; defaultContact: Contact | null;
allContacts: Contact[]; allContacts: Contact[];
}; };
@@ -96,23 +96,25 @@ export class CompanyController {
}, },
primaryContact: !opts?.includePrimaryContact primaryContact: !opts?.includePrimaryContact
? undefined ? undefined
: { : this.cw_Data?.defaultContact
firstName: this.cw_Data?.defaultContact.firstName, ? {
lastName: this.cw_Data?.defaultContact.lastName, firstName: this.cw_Data.defaultContact.firstName,
cwId: this.cw_Data?.defaultContact.id, lastName: this.cw_Data.defaultContact.lastName,
inactive: this.cw_Data?.defaultContact.inactiveFlag, cwId: this.cw_Data.defaultContact.id,
title: this.cw_Data?.defaultContact.title, inactive: this.cw_Data.defaultContact.inactiveFlag,
phone: this.cw_Data?.defaultContact.defaultPhoneNbr, title: this.cw_Data.defaultContact.title,
email: (() => { phone: this.cw_Data.defaultContact.defaultPhoneNbr,
if (!this.cw_Data?.defaultContact.communicationItems) email: (() => {
return null; if (!this.cw_Data?.defaultContact?.communicationItems)
return ( return null;
this.cw_Data?.defaultContact.communicationItems.find( return (
(v) => v.type.name === "Email", this.cw_Data.defaultContact.communicationItems.find(
)?.value ?? null (v) => v.type.name === "Email",
); )?.value ?? null
})(), );
}, })(),
}
: null,
allContacts: !opts?.includeAllContacts allContacts: !opts?.includeAllContacts
? undefined ? undefined
: this.cw_Data?.allContacts.map((contact) => ({ : this.cw_Data?.allContacts.map((contact) => ({
+290
View File
@@ -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,
};
}
}
+50 -1
View File
@@ -178,6 +178,46 @@ export default class UserController {
return decoded.permissions; 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 * Fetch Roles
* *
@@ -262,7 +302,16 @@ export default class UserController {
: this._roles.size > 0 : this._roles.size > 0
? this._roles.map((v) => v.moniker) ? this._roles.map((v) => v.moniker)
: undefined, : 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, login: opts?.safeReturn ? undefined : this.login,
email: opts?.safeReturn ? undefined : this.email, email: opts?.safeReturn ? undefined : this.email,
image: this.image, image: this.image,
+7
View File
@@ -12,6 +12,7 @@ import { unifiSites } from "./managers/unifiSites";
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies"; import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog"; import { refreshCatalog } from "./modules/cw-utils/procurement/refreshCatalog";
import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory"; import { refreshInventory } from "./modules/cw-utils/procurement/refreshInventory";
import { refreshOpportunities } from "./modules/cw-utils/opportunities/refreshOpportunities";
import { events, setupEventDebugger } from "./modules/globalEvents"; import { events, setupEventDebugger } from "./modules/globalEvents";
import { signPermissions } from "./modules/permission-utils/signPermissions"; import { signPermissions } from "./modules/permission-utils/signPermissions";
import { RoleController } from "./controllers/RoleController"; import { RoleController } from "./controllers/RoleController";
@@ -65,6 +66,12 @@ setInterval(
2 * 60 * 1000, 2 * 60 * 1000,
); );
// Refresh opportunities every minute
await refreshOpportunities();
setInterval(() => {
return refreshOpportunities();
}, 60 * 1000);
await unifiSites.syncSites(); await unifiSites.syncSites();
setInterval(() => { setInterval(() => {
return unifiSites.syncSites(); return unifiSites.syncSites();
+7 -4
View File
@@ -15,16 +15,19 @@ export const companies = {
const freshCwData: { data: Company } = await connectWiseApi.get( const freshCwData: { data: Company } = await connectWiseApi.get(
`/company/companies/${search.cw_CompanyId}`, `/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( const allContactsData = await connectWiseApi.get(
`${freshCwData.data._info.contacts_href}&pageSize=1000`, `${freshCwData.data._info.contacts_href}&pageSize=1000`,
); );
return new CompanyController(search, { return new CompanyController(search, {
company: freshCwData.data, company: freshCwData.data,
defaultContact: defaultContactData.data, defaultContact: defaultContactData?.data ?? null,
allContacts: allContactsData.data, allContacts: allContactsData.data,
}); });
}, },
+138
View File
@@ -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));
},
};
+171
View File
@@ -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 }, where: { cwCatalogId: cwId },
create: { create: {
cwCatalogId: cwId, cwCatalogId: cwId,
identifier: item.identifier,
name: item.description, name: item.description,
description: item.description, description: item.description,
customerDescription: item.customerDescription, customerDescription: item.customerDescription,
@@ -110,6 +111,7 @@ export const refreshCatalog = async () => {
}, },
update: { update: {
name: item.description, name: item.description,
identifier: item.identifier,
description: item.description, description: item.description,
customerDescription: item.customerDescription, customerDescription: item.customerDescription,
internalNotes: item.notes, internalNotes: item.notes,
+19
View File
@@ -158,6 +158,25 @@ interface EventTypes {
totalItems: number; totalItems: number;
updatedCount: number; updatedCount: number;
}) => void; }) => 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>(); export const events = new Eventra<EventTypes>();
+66
View File
@@ -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: { unifi: {
name: "UniFi Permissions", name: "UniFi Permissions",
description: description: