Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 212369ff63 | |||
| ffa463ab1a | |||
| 21cba28557 | |||
| fad8143ead | |||
| db727e0a9d | |||
| c3d55b898f | |||
| 6eee7bf0da | |||
| 5194d0e21e | |||
| c94de8198f | |||
| a55850e2c1 | |||
| 38654601c9 | |||
| 3db045289c | |||
| cdd9ad64eb | |||
| f91d8debcb | |||
| 5141ed20f9 | |||
| a8c48e8c75 | |||
| 051edb5f78 | |||
| f87f6dd336 | |||
| 2eb387811d | |||
| db27c9224d | |||
| 7f6e6fdfbc | |||
| 5f5f610060 | |||
| 809841d672 | |||
| 276eb563bf | |||
| 7624ba0bc0 | |||
| 1063231107 | |||
| 2cd5dee612 | |||
| 8ac1cbaf3e | |||
| bd7e6a37cd | |||
| 4e0799f9d9 | |||
| 223a06ba27 | |||
| 503657d168 | |||
| cf68e281e8 | |||
| 57b5763d41 | |||
| 2bd498a35d |
@@ -1,133 +0,0 @@
|
||||
name: API - Build and Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
steps:x
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.6"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
- name: Run tests
|
||||
run: bun test --preload ./tests/setup.ts
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push the Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api
|
||||
push: true
|
||||
target: runtime
|
||||
tags: |
|
||||
ghcr.io/project-optima/ttscm-api:latest
|
||||
ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Build and push the migration image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api
|
||||
push: true
|
||||
target: migration
|
||||
tags: |
|
||||
ghcr.io/project-optima/ttscm-api-migrate:latest
|
||||
ghcr.io/project-optima/ttscm-api-migrate:${{ github.event.release.tag_name }}
|
||||
|
||||
migrate:
|
||||
name: Run Migrations
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
steps:
|
||||
- name: Set the Kubernetes context
|
||||
uses: azure/k8s-set-context@v2
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.KUBECONFIG }}
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Delete previous migration job if exists
|
||||
run: kubectl delete job -n optima -l app=prisma-migrate --ignore-not-found
|
||||
|
||||
- name: Apply migration job
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
sed "s/RELEASE_TAG/${TAG}/g" kubernetes/migration-job.yaml | kubectl apply -f -
|
||||
|
||||
- name: Wait for migration to complete
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
kubectl wait --for=condition=complete --timeout=120s -n optima job/prisma-migrate-${TAG}
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: [migrate]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set the Kubernetes context
|
||||
uses: azure/k8s-set-context@v2
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.KUBECONFIG }}
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Lint Kubernetes manifests
|
||||
uses: azure/k8s-lint@v3
|
||||
with:
|
||||
lintType: dryrun
|
||||
manifests: |
|
||||
api/kubernetes/deployment.yaml
|
||||
api/kubernetes/ingress.yaml
|
||||
namespace: optima
|
||||
|
||||
- name: Deploy to the Kubernetes cluster
|
||||
uses: azure/k8s-deploy@v5
|
||||
with:
|
||||
namespace: optima
|
||||
force: true
|
||||
skip-tls-verify: true
|
||||
manifests: |
|
||||
api/kubernetes/deployment.yaml
|
||||
api/kubernetes/ingress.yaml
|
||||
images: |
|
||||
ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }}
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
bun-version: "1.3.6"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Generate API Prisma client
|
||||
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
bun-version: "1.3.6"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Generate Dalpuri Prisma client (CW MSSQL)
|
||||
run: DATABASE_URL="sqlserver://localhost:1433;database=dummy;user=dummy;password=dummy;trustServerCertificate=true" bunx prisma generate
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
bun-version: "1.3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun run test:unit -- --run
|
||||
|
||||
+32
-9
@@ -17,7 +17,7 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install --frozen-lockfile --production
|
||||
RUN bun install --production
|
||||
|
||||
# ---- Stage 2: Build ----
|
||||
FROM oven/bun:1.3.11 AS build
|
||||
@@ -32,7 +32,7 @@ COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
# Install all deps (including dev) for the full workspace
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun install
|
||||
|
||||
# Copy API source and config
|
||||
COPY api/src/ ./api/src/
|
||||
@@ -90,6 +90,10 @@ COPY --from=build /app/dalpuri/generated/ ./dalpuri/generated/
|
||||
# Copy production node_modules (Prisma adapter needs native bindings)
|
||||
COPY --from=deps /app/node_modules/ ./node_modules/
|
||||
|
||||
# Copy bun so prisma migrate deploy can run at container startup
|
||||
COPY --from=build /usr/local/bin/bun /usr/local/bin/bun
|
||||
RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx
|
||||
|
||||
# Ensure pdfmake Roboto fonts are present at runtime for PDF generation.
|
||||
COPY --from=build /app/api/node_modules/pdfmake/build/fonts/ ./node_modules/pdfmake/build/fonts/
|
||||
|
||||
@@ -104,7 +108,7 @@ COPY --from=build /app/api/logo.png ./logo.png
|
||||
COPY --from=build /app/api/src/modules/sales-utils/salesTaxRates.json ./salesTaxRates.json
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["./server"]
|
||||
CMD ["sh", "-c", "bunx prisma migrate deploy && ./server"]
|
||||
|
||||
# ---- Stage 5: Worker runtime image ----
|
||||
FROM runtime-base AS worker
|
||||
@@ -128,15 +132,13 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun install
|
||||
|
||||
COPY api/prisma/ ./api/prisma/
|
||||
COPY api/prisma.config.ts ./api/prisma.config.ts
|
||||
|
||||
RUN chmod +x /app/api/prisma/migrate-entrypoint.sh
|
||||
|
||||
WORKDIR /app/api
|
||||
CMD ["sh", "prisma/migrate-entrypoint.sh"]
|
||||
CMD ["bunx", "prisma", "migrate", "deploy"]
|
||||
|
||||
# ---- Stage 7: Dalpuri CW-to-API sync runner ----
|
||||
FROM oven/bun:1.3.11 AS dalpuri-sync
|
||||
@@ -149,7 +151,7 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun install
|
||||
|
||||
COPY dalpuri/src/ ./dalpuri/src/
|
||||
COPY dalpuri/prisma/ ./dalpuri/prisma/
|
||||
@@ -166,4 +168,25 @@ WORKDIR /app/api
|
||||
RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
WORKDIR /app/dalpuri
|
||||
CMD ["bun", "run", "src/sync.ts"]
|
||||
CMD ["bun", "run", "src/sync.ts"]
|
||||
|
||||
FROM oven/bun:1.3.11 AS setup-admin
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY api/package.json ./api/package.json
|
||||
COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install
|
||||
|
||||
COPY api/prisma/ ./api/prisma/
|
||||
COPY api/prisma.config.ts ./api/prisma.config.ts
|
||||
COPY api/setup-admin.ts ./api/setup-admin.ts
|
||||
|
||||
WORKDIR /app/api
|
||||
RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
CMD ["bun", "run", "setup-admin.ts"]
|
||||
|
||||
@@ -164,6 +164,7 @@ Permissions for accessing and managing sales opportunities. Opportunities are sy
|
||||
| `sales.opportunity.product.add.labor` | Add labor products via the dedicated labor route with Field/Tech catalog selection and labor pricing inputs. | [src/api/sales/opportunities/[id]/products/addLabor.ts](src/api/sales/opportunities/[id]/products/addLabor.ts), [src/api/sales/opportunities/[id]/products/laborOptions.ts](src/api/sales/opportunities/[id]/products/laborOptions.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.quote.fetch` | Fetch all committed quotes for an opportunity. | [src/api/sales/opportunities/[id]/quotes/fetchAll.ts](src/api/sales/opportunities/[id]/quotes/fetchAll.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.quote.commit` | Generate and store a finalized quote PDF for an opportunity with regeneration metadata and creator attribution. | [src/api/sales/opportunities/[id]/quotes/commit.ts](src/api/sales/opportunities/[id]/quotes/commit.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.quote.commit.backgenerate` | Generate a quote on an opportunity that is in a workflow state other than New or Active (e.g. PendingWon, QuoteSent). Without this permission, quote generation is restricted to the New and Active states only. | [src/api/sales/opportunities/[id]/quotes/commit.ts](src/api/sales/opportunities/[id]/quotes/commit.ts) | `sales.opportunity.quote.commit` |
|
||||
| `sales.opportunity.quote.preview` | Generate a preview-stamped quote PDF for an opportunity without storing it. | [src/api/sales/opportunities/[id]/quotes/preview.ts](src/api/sales/opportunities/[id]/quotes/preview.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.quote.download` | Download a committed quote PDF. Each download is recorded with timestamp and user info. | [src/api/sales/opportunities/[id]/quotes/download.ts](src/api/sales/opportunities/[id]/quotes/download.ts) | `sales.opportunity.fetch` |
|
||||
| `sales.opportunity.quote.fetch_downloads` | Fetch download/print history for all quotes on an opportunity. Admin-level permission. | [src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts](src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts) | `sales.opportunity.fetch` |
|
||||
|
||||
@@ -212,6 +212,51 @@ export type ScheduleSpan = Prisma.ScheduleSpanModel
|
||||
*
|
||||
*/
|
||||
export type Schedule = Prisma.ScheduleModel
|
||||
/**
|
||||
* Model Activity
|
||||
*
|
||||
*/
|
||||
export type Activity = Prisma.ActivityModel
|
||||
/**
|
||||
* Model ActivityNotes
|
||||
*
|
||||
*/
|
||||
export type ActivityNotes = Prisma.ActivityNotesModel
|
||||
/**
|
||||
* Model ActivityType
|
||||
*
|
||||
*/
|
||||
export type ActivityType = Prisma.ActivityTypeModel
|
||||
/**
|
||||
* Model ActivityStatus
|
||||
*
|
||||
*/
|
||||
export type ActivityStatus = Prisma.ActivityStatusModel
|
||||
/**
|
||||
* Model TimeEntry
|
||||
*
|
||||
*/
|
||||
export type TimeEntry = Prisma.TimeEntryModel
|
||||
/**
|
||||
* Model TimeEntryStatus
|
||||
*
|
||||
*/
|
||||
export type TimeEntryStatus = Prisma.TimeEntryStatusModel
|
||||
/**
|
||||
* Model TimeEntryChargeCode
|
||||
*
|
||||
*/
|
||||
export type TimeEntryChargeCode = Prisma.TimeEntryChargeCodeModel
|
||||
/**
|
||||
* Model TimeActivityClass
|
||||
*
|
||||
*/
|
||||
export type TimeActivityClass = Prisma.TimeActivityClassModel
|
||||
/**
|
||||
* Model TimeActivityType
|
||||
*
|
||||
*/
|
||||
export type TimeActivityType = Prisma.TimeActivityTypeModel
|
||||
/**
|
||||
* Model CredentialType
|
||||
*
|
||||
@@ -242,3 +287,8 @@ export type TaxCode = Prisma.TaxCodeModel
|
||||
*
|
||||
*/
|
||||
export type CwMember = Prisma.CwMemberModel
|
||||
/**
|
||||
* Model CwMemberType
|
||||
*
|
||||
*/
|
||||
export type CwMemberType = Prisma.CwMemberTypeModel
|
||||
|
||||
@@ -236,6 +236,51 @@ export type ScheduleSpan = Prisma.ScheduleSpanModel
|
||||
*
|
||||
*/
|
||||
export type Schedule = Prisma.ScheduleModel
|
||||
/**
|
||||
* Model Activity
|
||||
*
|
||||
*/
|
||||
export type Activity = Prisma.ActivityModel
|
||||
/**
|
||||
* Model ActivityNotes
|
||||
*
|
||||
*/
|
||||
export type ActivityNotes = Prisma.ActivityNotesModel
|
||||
/**
|
||||
* Model ActivityType
|
||||
*
|
||||
*/
|
||||
export type ActivityType = Prisma.ActivityTypeModel
|
||||
/**
|
||||
* Model ActivityStatus
|
||||
*
|
||||
*/
|
||||
export type ActivityStatus = Prisma.ActivityStatusModel
|
||||
/**
|
||||
* Model TimeEntry
|
||||
*
|
||||
*/
|
||||
export type TimeEntry = Prisma.TimeEntryModel
|
||||
/**
|
||||
* Model TimeEntryStatus
|
||||
*
|
||||
*/
|
||||
export type TimeEntryStatus = Prisma.TimeEntryStatusModel
|
||||
/**
|
||||
* Model TimeEntryChargeCode
|
||||
*
|
||||
*/
|
||||
export type TimeEntryChargeCode = Prisma.TimeEntryChargeCodeModel
|
||||
/**
|
||||
* Model TimeActivityClass
|
||||
*
|
||||
*/
|
||||
export type TimeActivityClass = Prisma.TimeActivityClassModel
|
||||
/**
|
||||
* Model TimeActivityType
|
||||
*
|
||||
*/
|
||||
export type TimeActivityType = Prisma.TimeActivityTypeModel
|
||||
/**
|
||||
* Model CredentialType
|
||||
*
|
||||
@@ -266,3 +311,8 @@ export type TaxCode = Prisma.TaxCodeModel
|
||||
*
|
||||
*/
|
||||
export type CwMember = Prisma.CwMemberModel
|
||||
/**
|
||||
* Model CwMemberType
|
||||
*
|
||||
*/
|
||||
export type CwMemberType = Prisma.CwMemberTypeModel
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -90,12 +90,22 @@ export const ModelName = {
|
||||
ScheduleType: 'ScheduleType',
|
||||
ScheduleSpan: 'ScheduleSpan',
|
||||
Schedule: 'Schedule',
|
||||
Activity: 'Activity',
|
||||
ActivityNotes: 'ActivityNotes',
|
||||
ActivityType: 'ActivityType',
|
||||
ActivityStatus: 'ActivityStatus',
|
||||
TimeEntry: 'TimeEntry',
|
||||
TimeEntryStatus: 'TimeEntryStatus',
|
||||
TimeEntryChargeCode: 'TimeEntryChargeCode',
|
||||
TimeActivityClass: 'TimeActivityClass',
|
||||
TimeActivityType: 'TimeActivityType',
|
||||
CredentialType: 'CredentialType',
|
||||
SecureValue: 'SecureValue',
|
||||
Credential: 'Credential',
|
||||
GeneratedQuotes: 'GeneratedQuotes',
|
||||
TaxCode: 'TaxCode',
|
||||
CwMember: 'CwMember'
|
||||
CwMember: 'CwMember',
|
||||
CwMemberType: 'CwMemberType'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
@@ -888,6 +898,204 @@ export const ScheduleScalarFieldEnum = {
|
||||
export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typeof ScheduleScalarFieldEnum]
|
||||
|
||||
|
||||
export const ActivityScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
subject: 'subject',
|
||||
startTime: 'startTime',
|
||||
endTime: 'endTime',
|
||||
assignToId: 'assignToId',
|
||||
assignedById: 'assignedById',
|
||||
enteredBy: 'enteredBy',
|
||||
automated: 'automated',
|
||||
closedFlag: 'closedFlag',
|
||||
notifyCompleteFlag: 'notifyCompleteFlag',
|
||||
notificationSentFlat: 'notificationSentFlat',
|
||||
opportunityId: 'opportunityId',
|
||||
serviceTicketId: 'serviceTicketId',
|
||||
contactId: 'contactId',
|
||||
companyId: 'companyId',
|
||||
activityTypeId: 'activityTypeId',
|
||||
activityStatusId: 'activityStatusId',
|
||||
createdById: 'createdById',
|
||||
updatedById: 'updatedById',
|
||||
closedById: 'closedById',
|
||||
closedAt: 'closedAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type ActivityScalarFieldEnum = (typeof ActivityScalarFieldEnum)[keyof typeof ActivityScalarFieldEnum]
|
||||
|
||||
|
||||
export const ActivityNotesScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
notes: 'notes',
|
||||
activityId: 'activityId',
|
||||
internalAnalysisFlag: 'internalAnalysisFlag',
|
||||
enteredById: 'enteredById',
|
||||
updatedById: 'updatedById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type ActivityNotesScalarFieldEnum = (typeof ActivityNotesScalarFieldEnum)[keyof typeof ActivityNotesScalarFieldEnum]
|
||||
|
||||
|
||||
export const ActivityTypeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
inactiveFlag: 'inactiveFlag',
|
||||
historyFlag: 'historyFlag',
|
||||
defaultFlag: 'defaultFlag',
|
||||
importFlag: 'importFlag',
|
||||
emailFlag: 'emailFlag',
|
||||
memoFlag: 'memoFlag',
|
||||
pointsValue: 'pointsValue',
|
||||
updatedById: 'updatedById',
|
||||
createdById: 'createdById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type ActivityTypeScalarFieldEnum = (typeof ActivityTypeScalarFieldEnum)[keyof typeof ActivityTypeScalarFieldEnum]
|
||||
|
||||
|
||||
export const ActivityStatusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
closedFlag: 'closedFlag',
|
||||
inactiveFlag: 'inactiveFlag',
|
||||
defaultFlag: 'defaultFlag',
|
||||
spawnFollowupFlag: 'spawnFollowupFlag',
|
||||
updatedById: 'updatedById',
|
||||
createdById: 'createdById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type ActivityStatusScalarFieldEnum = (typeof ActivityStatusScalarFieldEnum)[keyof typeof ActivityStatusScalarFieldEnum]
|
||||
|
||||
|
||||
export const TimeEntryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
memberId: 'memberId',
|
||||
serviceTicketId: 'serviceTicketId',
|
||||
activityId: 'activityId',
|
||||
projectId: 'projectId',
|
||||
chargeCodeId: 'chargeCodeId',
|
||||
companyId: 'companyId',
|
||||
statusId: 'statusId',
|
||||
locationId: 'locationId',
|
||||
contactId: 'contactId',
|
||||
dateStart: 'dateStart',
|
||||
timeStart: 'timeStart',
|
||||
timeEnd: 'timeEnd',
|
||||
notes: 'notes',
|
||||
notesMd: 'notesMd',
|
||||
internalNote: 'internalNote',
|
||||
billableHours: 'billableHours',
|
||||
actualHours: 'actualHours',
|
||||
invoicedHours: 'invoicedHours',
|
||||
deductedHours: 'deductedHours',
|
||||
hourlyRate: 'hourlyRate',
|
||||
effectiveRate: 'effectiveRate',
|
||||
issueFlag: 'issueFlag',
|
||||
mergedFlag: 'mergedFlag',
|
||||
invoiceFlag: 'invoiceFlag',
|
||||
billableFlag: 'billableFlag',
|
||||
documentFlag: 'documentFlag',
|
||||
teProblemFlag: 'teProblemFlag',
|
||||
teResolutionFlag: 'teResolutionFlag',
|
||||
teInternalAnalysisFlag: 'teInternalAnalysisFlag',
|
||||
chargeToRecId: 'chargeToRecId',
|
||||
chargeToType: 'chargeToType',
|
||||
createdById: 'createdById',
|
||||
updatedById: 'updatedById',
|
||||
originalAuthorId: 'originalAuthorId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type TimeEntryScalarFieldEnum = (typeof TimeEntryScalarFieldEnum)[keyof typeof TimeEntryScalarFieldEnum]
|
||||
|
||||
|
||||
export const TimeEntryStatusScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
statusId: 'statusId',
|
||||
description: 'description',
|
||||
action: 'action',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type TimeEntryStatusScalarFieldEnum = (typeof TimeEntryStatusScalarFieldEnum)[keyof typeof TimeEntryStatusScalarFieldEnum]
|
||||
|
||||
|
||||
export const TimeEntryChargeCodeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
chargeCodeId: 'chargeCodeId',
|
||||
description: 'description',
|
||||
expenseFlag: 'expenseFlag',
|
||||
timeFlag: 'timeFlag',
|
||||
billableFlag: 'billableFlag',
|
||||
invoiceFlag: 'invoiceFlag',
|
||||
updatedById: 'updatedById',
|
||||
createdById: 'createdById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type TimeEntryChargeCodeScalarFieldEnum = (typeof TimeEntryChargeCodeScalarFieldEnum)[keyof typeof TimeEntryChargeCodeScalarFieldEnum]
|
||||
|
||||
|
||||
export const TimeActivityClassScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
description: 'description',
|
||||
hourlyRate: 'hourlyRate',
|
||||
inactiveFlag: 'inactiveFlag',
|
||||
taxExemptFlag: 'taxExemptFlag',
|
||||
createdById: 'createdById',
|
||||
updatedById: 'updatedById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type TimeActivityClassScalarFieldEnum = (typeof TimeActivityClassScalarFieldEnum)[keyof typeof TimeActivityClassScalarFieldEnum]
|
||||
|
||||
|
||||
export const TimeActivityTypeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
description: 'description',
|
||||
minHours: 'minHours',
|
||||
maxHours: 'maxHours',
|
||||
rate: 'rate',
|
||||
costMultiplier: 'costMultiplier',
|
||||
inactiveFlag: 'inactiveFlag',
|
||||
invoiceFlag: 'invoiceFlag',
|
||||
billableFlag: 'billableFlag',
|
||||
utilizationFlag: 'utilizationFlag',
|
||||
defaultFlag: 'defaultFlag',
|
||||
multiplierFlag: 'multiplierFlag',
|
||||
createdById: 'createdById',
|
||||
updatedById: 'updatedById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type TimeActivityTypeScalarFieldEnum = (typeof TimeActivityTypeScalarFieldEnum)[keyof typeof TimeActivityTypeScalarFieldEnum]
|
||||
|
||||
|
||||
export const CredentialTypeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
@@ -980,6 +1188,20 @@ export const CwMemberScalarFieldEnum = {
|
||||
export type CwMemberScalarFieldEnum = (typeof CwMemberScalarFieldEnum)[keyof typeof CwMemberScalarFieldEnum]
|
||||
|
||||
|
||||
export const CwMemberTypeScalarFieldEnum = {
|
||||
id: 'id',
|
||||
uid: 'uid',
|
||||
description: 'description',
|
||||
inactiveFlag: 'inactiveFlag',
|
||||
updatedById: 'updatedById',
|
||||
createdById: 'createdById',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type CwMemberTypeScalarFieldEnum = (typeof CwMemberTypeScalarFieldEnum)[keyof typeof CwMemberTypeScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
|
||||
@@ -47,10 +47,20 @@ export type * from './models/ScheduleStatus.ts'
|
||||
export type * from './models/ScheduleType.ts'
|
||||
export type * from './models/ScheduleSpan.ts'
|
||||
export type * from './models/Schedule.ts'
|
||||
export type * from './models/Activity.ts'
|
||||
export type * from './models/ActivityNotes.ts'
|
||||
export type * from './models/ActivityType.ts'
|
||||
export type * from './models/ActivityStatus.ts'
|
||||
export type * from './models/TimeEntry.ts'
|
||||
export type * from './models/TimeEntryStatus.ts'
|
||||
export type * from './models/TimeEntryChargeCode.ts'
|
||||
export type * from './models/TimeActivityClass.ts'
|
||||
export type * from './models/TimeActivityType.ts'
|
||||
export type * from './models/CredentialType.ts'
|
||||
export type * from './models/SecureValue.ts'
|
||||
export type * from './models/Credential.ts'
|
||||
export type * from './models/GeneratedQuotes.ts'
|
||||
export type * from './models/TaxCode.ts'
|
||||
export type * from './models/CwMember.ts'
|
||||
export type * from './models/CwMemberType.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
@@ -293,6 +293,8 @@ export type CompanyWhereInput = {
|
||||
credentials?: Prisma.CredentialListRelationFilter
|
||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||
opportunities?: Prisma.OpportunityListRelationFilter
|
||||
timeEntries?: Prisma.TimeEntryListRelationFilter
|
||||
activities?: Prisma.ActivityListRelationFilter
|
||||
deletedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
enteredBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
serviceTickets?: Prisma.ServiceTicketListRelationFilter
|
||||
@@ -319,6 +321,8 @@ export type CompanyOrderByWithRelationInput = {
|
||||
credentials?: Prisma.CredentialOrderByRelationAggregateInput
|
||||
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
|
||||
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
|
||||
timeEntries?: Prisma.TimeEntryOrderByRelationAggregateInput
|
||||
activities?: Prisma.ActivityOrderByRelationAggregateInput
|
||||
deletedBy?: Prisma.UserOrderByWithRelationInput
|
||||
enteredBy?: Prisma.UserOrderByWithRelationInput
|
||||
serviceTickets?: Prisma.ServiceTicketOrderByRelationAggregateInput
|
||||
@@ -348,6 +352,8 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
|
||||
credentials?: Prisma.CredentialListRelationFilter
|
||||
unifiSites?: Prisma.UnifiSiteListRelationFilter
|
||||
opportunities?: Prisma.OpportunityListRelationFilter
|
||||
timeEntries?: Prisma.TimeEntryListRelationFilter
|
||||
activities?: Prisma.ActivityListRelationFilter
|
||||
deletedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
enteredBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
|
||||
serviceTickets?: Prisma.ServiceTicketListRelationFilter
|
||||
@@ -414,6 +420,8 @@ export type CompanyCreateInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -440,6 +448,8 @@ export type CompanyUncheckedCreateInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -462,6 +472,8 @@ export type CompanyUpdateInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -488,6 +500,8 @@ export type CompanyUncheckedUpdateInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -798,6 +812,36 @@ export type CompanyUpdateOneWithoutOpportunitiesNestedInput = {
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutOpportunitiesInput, Prisma.CompanyUpdateWithoutOpportunitiesInput>, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyCreateNestedOneWithoutActivitiesInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutActivitiesInput, Prisma.CompanyUncheckedCreateWithoutActivitiesInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutActivitiesInput
|
||||
connect?: Prisma.CompanyWhereUniqueInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateOneWithoutActivitiesNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutActivitiesInput, Prisma.CompanyUncheckedCreateWithoutActivitiesInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutActivitiesInput
|
||||
upsert?: Prisma.CompanyUpsertWithoutActivitiesInput
|
||||
disconnect?: Prisma.CompanyWhereInput | boolean
|
||||
delete?: Prisma.CompanyWhereInput | boolean
|
||||
connect?: Prisma.CompanyWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutActivitiesInput, Prisma.CompanyUpdateWithoutActivitiesInput>, Prisma.CompanyUncheckedUpdateWithoutActivitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyCreateNestedOneWithoutTimeEntriesInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutTimeEntriesInput, Prisma.CompanyUncheckedCreateWithoutTimeEntriesInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutTimeEntriesInput
|
||||
connect?: Prisma.CompanyWhereUniqueInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateOneRequiredWithoutTimeEntriesNestedInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutTimeEntriesInput, Prisma.CompanyUncheckedCreateWithoutTimeEntriesInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutTimeEntriesInput
|
||||
upsert?: Prisma.CompanyUpsertWithoutTimeEntriesInput
|
||||
connect?: Prisma.CompanyWhereUniqueInput
|
||||
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutTimeEntriesInput, Prisma.CompanyUpdateWithoutTimeEntriesInput>, Prisma.CompanyUncheckedUpdateWithoutTimeEntriesInput>
|
||||
}
|
||||
|
||||
export type CompanyCreateNestedOneWithoutCredentialsInput = {
|
||||
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
|
||||
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
|
||||
@@ -830,6 +874,8 @@ export type CompanyCreateWithoutDeletedByInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
|
||||
@@ -854,6 +900,8 @@ export type CompanyUncheckedCreateWithoutDeletedByInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -886,6 +934,8 @@ export type CompanyCreateWithoutEnteredByInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
|
||||
@@ -910,6 +960,8 @@ export type CompanyUncheckedCreateWithoutEnteredByInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -993,6 +1045,8 @@ export type CompanyCreateWithoutUnifiSitesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -1018,6 +1072,8 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -1055,6 +1111,8 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -1080,6 +1138,8 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1101,6 +1161,8 @@ export type CompanyCreateWithoutCompanyAddressesInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -1126,6 +1188,8 @@ export type CompanyUncheckedCreateWithoutCompanyAddressesInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -1163,6 +1227,8 @@ export type CompanyUpdateWithoutCompanyAddressesInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -1188,6 +1254,8 @@ export type CompanyUncheckedUpdateWithoutCompanyAddressesInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1209,6 +1277,8 @@ export type CompanyCreateWithoutContactsInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -1234,6 +1304,8 @@ export type CompanyUncheckedCreateWithoutContactsInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -1271,6 +1343,8 @@ export type CompanyUpdateWithoutContactsInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -1296,6 +1370,8 @@ export type CompanyUncheckedUpdateWithoutContactsInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1318,6 +1394,8 @@ export type CompanyCreateWithoutServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
|
||||
@@ -1343,6 +1421,8 @@ export type CompanyUncheckedCreateWithoutServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
|
||||
@@ -1369,6 +1449,8 @@ export type CompanyCreateWithoutBillingServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -1394,6 +1476,8 @@ export type CompanyUncheckedCreateWithoutBillingServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
}
|
||||
|
||||
@@ -1431,6 +1515,8 @@ export type CompanyUpdateWithoutServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
|
||||
@@ -1456,6 +1542,8 @@ export type CompanyUncheckedUpdateWithoutServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
|
||||
@@ -1488,6 +1576,8 @@ export type CompanyUpdateWithoutBillingServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -1513,6 +1603,8 @@ export type CompanyUncheckedUpdateWithoutBillingServiceTicketsInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
}
|
||||
|
||||
@@ -1533,6 +1625,8 @@ export type CompanyCreateWithoutOpportunitiesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -1558,6 +1652,8 @@ export type CompanyUncheckedCreateWithoutOpportunitiesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -1595,6 +1691,8 @@ export type CompanyUpdateWithoutOpportunitiesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -1620,6 +1718,240 @@ export type CompanyUncheckedUpdateWithoutOpportunitiesInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyCreateWithoutActivitiesInput = {
|
||||
id: number
|
||||
uid?: string
|
||||
name: string
|
||||
phone?: string | null
|
||||
website?: string | null
|
||||
deleteFlag?: boolean
|
||||
dateDeleted?: Date | string | null
|
||||
taxId?: string | null
|
||||
taxExempt?: boolean
|
||||
deletedAt?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
contacts?: Prisma.ContactCreateNestedManyWithoutCompanyInput
|
||||
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedCreateWithoutActivitiesInput = {
|
||||
id: number
|
||||
uid?: string
|
||||
name: string
|
||||
phone?: string | null
|
||||
website?: string | null
|
||||
deleteFlag?: boolean
|
||||
dateDeleted?: Date | string | null
|
||||
taxId?: string | null
|
||||
taxExempt?: boolean
|
||||
enteredById?: string | null
|
||||
deletedById?: string | null
|
||||
deletedAt?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
contacts?: Prisma.ContactUncheckedCreateNestedManyWithoutCompanyInput
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyCreateOrConnectWithoutActivitiesInput = {
|
||||
where: Prisma.CompanyWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.CompanyCreateWithoutActivitiesInput, Prisma.CompanyUncheckedCreateWithoutActivitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyUpsertWithoutActivitiesInput = {
|
||||
update: Prisma.XOR<Prisma.CompanyUpdateWithoutActivitiesInput, Prisma.CompanyUncheckedUpdateWithoutActivitiesInput>
|
||||
create: Prisma.XOR<Prisma.CompanyCreateWithoutActivitiesInput, Prisma.CompanyUncheckedCreateWithoutActivitiesInput>
|
||||
where?: Prisma.CompanyWhereInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateToOneWithWhereWithoutActivitiesInput = {
|
||||
where?: Prisma.CompanyWhereInput
|
||||
data: Prisma.XOR<Prisma.CompanyUpdateWithoutActivitiesInput, Prisma.CompanyUncheckedUpdateWithoutActivitiesInput>
|
||||
}
|
||||
|
||||
export type CompanyUpdateWithoutActivitiesInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
uid?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
website?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deleteFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
dateDeleted?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
taxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
taxExempt?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
contacts?: Prisma.ContactUpdateManyWithoutCompanyNestedInput
|
||||
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedUpdateWithoutActivitiesInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
uid?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
website?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deleteFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
dateDeleted?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
taxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
taxExempt?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
enteredById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deletedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
contacts?: Prisma.ContactUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyCreateWithoutTimeEntriesInput = {
|
||||
id: number
|
||||
uid?: string
|
||||
name: string
|
||||
phone?: string | null
|
||||
website?: string | null
|
||||
deleteFlag?: boolean
|
||||
dateDeleted?: Date | string | null
|
||||
taxId?: string | null
|
||||
taxExempt?: boolean
|
||||
deletedAt?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
contacts?: Prisma.ContactCreateNestedManyWithoutCompanyInput
|
||||
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedCreateWithoutTimeEntriesInput = {
|
||||
id: number
|
||||
uid?: string
|
||||
name: string
|
||||
phone?: string | null
|
||||
website?: string | null
|
||||
deleteFlag?: boolean
|
||||
dateDeleted?: Date | string | null
|
||||
taxId?: string | null
|
||||
taxExempt?: boolean
|
||||
enteredById?: string | null
|
||||
deletedById?: string | null
|
||||
deletedAt?: Date | string | null
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
contacts?: Prisma.ContactUncheckedCreateNestedManyWithoutCompanyInput
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
|
||||
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
|
||||
export type CompanyCreateOrConnectWithoutTimeEntriesInput = {
|
||||
where: Prisma.CompanyWhereUniqueInput
|
||||
create: Prisma.XOR<Prisma.CompanyCreateWithoutTimeEntriesInput, Prisma.CompanyUncheckedCreateWithoutTimeEntriesInput>
|
||||
}
|
||||
|
||||
export type CompanyUpsertWithoutTimeEntriesInput = {
|
||||
update: Prisma.XOR<Prisma.CompanyUpdateWithoutTimeEntriesInput, Prisma.CompanyUncheckedUpdateWithoutTimeEntriesInput>
|
||||
create: Prisma.XOR<Prisma.CompanyCreateWithoutTimeEntriesInput, Prisma.CompanyUncheckedCreateWithoutTimeEntriesInput>
|
||||
where?: Prisma.CompanyWhereInput
|
||||
}
|
||||
|
||||
export type CompanyUpdateToOneWithWhereWithoutTimeEntriesInput = {
|
||||
where?: Prisma.CompanyWhereInput
|
||||
data: Prisma.XOR<Prisma.CompanyUpdateWithoutTimeEntriesInput, Prisma.CompanyUncheckedUpdateWithoutTimeEntriesInput>
|
||||
}
|
||||
|
||||
export type CompanyUpdateWithoutTimeEntriesInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
uid?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
website?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deleteFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
dateDeleted?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
taxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
taxExempt?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
contacts?: Prisma.ContactUpdateManyWithoutCompanyNestedInput
|
||||
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
|
||||
export type CompanyUncheckedUpdateWithoutTimeEntriesInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
uid?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
website?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deleteFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
dateDeleted?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
taxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
taxExempt?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
enteredById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deletedById?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
contacts?: Prisma.ContactUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1641,6 +1973,8 @@ export type CompanyCreateWithoutCredentialsInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
|
||||
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
|
||||
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
|
||||
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
|
||||
@@ -1666,6 +2000,8 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
|
||||
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
|
||||
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
|
||||
}
|
||||
@@ -1703,6 +2039,8 @@ export type CompanyUpdateWithoutCredentialsInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
@@ -1728,6 +2066,8 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
|
||||
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1782,6 +2122,8 @@ export type CompanyUpdateWithoutDeletedByInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
|
||||
@@ -1806,6 +2148,8 @@ export type CompanyUncheckedUpdateWithoutDeletedByInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1844,6 +2188,8 @@ export type CompanyUpdateWithoutEnteredByInput = {
|
||||
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
|
||||
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
|
||||
@@ -1868,6 +2214,8 @@ export type CompanyUncheckedUpdateWithoutEnteredByInput = {
|
||||
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
|
||||
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
|
||||
}
|
||||
@@ -1899,6 +2247,8 @@ export type CompanyCountOutputType = {
|
||||
credentials: number
|
||||
unifiSites: number
|
||||
opportunities: number
|
||||
timeEntries: number
|
||||
activities: number
|
||||
serviceTickets: number
|
||||
billingServiceTickets: number
|
||||
}
|
||||
@@ -1909,6 +2259,8 @@ export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extension
|
||||
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
|
||||
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
|
||||
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
|
||||
timeEntries?: boolean | CompanyCountOutputTypeCountTimeEntriesArgs
|
||||
activities?: boolean | CompanyCountOutputTypeCountActivitiesArgs
|
||||
serviceTickets?: boolean | CompanyCountOutputTypeCountServiceTicketsArgs
|
||||
billingServiceTickets?: boolean | CompanyCountOutputTypeCountBillingServiceTicketsArgs
|
||||
}
|
||||
@@ -1958,6 +2310,20 @@ export type CompanyCountOutputTypeCountOpportunitiesArgs<ExtArgs extends runtime
|
||||
where?: Prisma.OpportunityWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* CompanyCountOutputType without action
|
||||
*/
|
||||
export type CompanyCountOutputTypeCountTimeEntriesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.TimeEntryWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* CompanyCountOutputType without action
|
||||
*/
|
||||
export type CompanyCountOutputTypeCountActivitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
where?: Prisma.ActivityWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* CompanyCountOutputType without action
|
||||
*/
|
||||
@@ -1993,6 +2359,8 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||
timeEntries?: boolean | Prisma.Company$timeEntriesArgs<ExtArgs>
|
||||
activities?: boolean | Prisma.Company$activitiesArgs<ExtArgs>
|
||||
deletedBy?: boolean | Prisma.Company$deletedByArgs<ExtArgs>
|
||||
enteredBy?: boolean | Prisma.Company$enteredByArgs<ExtArgs>
|
||||
serviceTickets?: boolean | Prisma.Company$serviceTicketsArgs<ExtArgs>
|
||||
@@ -2062,6 +2430,8 @@ export type CompanyInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
|
||||
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
|
||||
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
|
||||
timeEntries?: boolean | Prisma.Company$timeEntriesArgs<ExtArgs>
|
||||
activities?: boolean | Prisma.Company$activitiesArgs<ExtArgs>
|
||||
deletedBy?: boolean | Prisma.Company$deletedByArgs<ExtArgs>
|
||||
enteredBy?: boolean | Prisma.Company$enteredByArgs<ExtArgs>
|
||||
serviceTickets?: boolean | Prisma.Company$serviceTicketsArgs<ExtArgs>
|
||||
@@ -2085,6 +2455,8 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
||||
credentials: Prisma.$CredentialPayload<ExtArgs>[]
|
||||
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
|
||||
opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
|
||||
timeEntries: Prisma.$TimeEntryPayload<ExtArgs>[]
|
||||
activities: Prisma.$ActivityPayload<ExtArgs>[]
|
||||
deletedBy: Prisma.$UserPayload<ExtArgs> | null
|
||||
enteredBy: Prisma.$UserPayload<ExtArgs> | null
|
||||
serviceTickets: Prisma.$ServiceTicketPayload<ExtArgs>[]
|
||||
@@ -2504,6 +2876,8 @@ export interface Prisma__CompanyClient<T, Null = never, ExtArgs extends runtime.
|
||||
credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
opportunities<T extends Prisma.Company$opportunitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$opportunitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OpportunityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
timeEntries<T extends Prisma.Company$timeEntriesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$timeEntriesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TimeEntryPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
activities<T extends Prisma.Company$activitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$activitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ActivityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
deletedBy<T extends Prisma.Company$deletedByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$deletedByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
enteredBy<T extends Prisma.Company$enteredByArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$enteredByArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
serviceTickets<T extends Prisma.Company$serviceTicketsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$serviceTicketsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServiceTicketPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
@@ -3071,6 +3445,54 @@ export type Company$opportunitiesArgs<ExtArgs extends runtime.Types.Extensions.I
|
||||
distinct?: Prisma.OpportunityScalarFieldEnum | Prisma.OpportunityScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Company.timeEntries
|
||||
*/
|
||||
export type Company$timeEntriesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the TimeEntry
|
||||
*/
|
||||
select?: Prisma.TimeEntrySelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the TimeEntry
|
||||
*/
|
||||
omit?: Prisma.TimeEntryOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.TimeEntryInclude<ExtArgs> | null
|
||||
where?: Prisma.TimeEntryWhereInput
|
||||
orderBy?: Prisma.TimeEntryOrderByWithRelationInput | Prisma.TimeEntryOrderByWithRelationInput[]
|
||||
cursor?: Prisma.TimeEntryWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.TimeEntryScalarFieldEnum | Prisma.TimeEntryScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Company.activities
|
||||
*/
|
||||
export type Company$activitiesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the Activity
|
||||
*/
|
||||
select?: Prisma.ActivitySelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the Activity
|
||||
*/
|
||||
omit?: Prisma.ActivityOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.ActivityInclude<ExtArgs> | null
|
||||
where?: Prisma.ActivityWhereInput
|
||||
orderBy?: Prisma.ActivityOrderByWithRelationInput | Prisma.ActivityOrderByWithRelationInput[]
|
||||
cursor?: Prisma.ActivityWhereUniqueInput
|
||||
take?: number
|
||||
skip?: number
|
||||
distinct?: Prisma.ActivityScalarFieldEnum | Prisma.ActivityScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Company.deletedBy
|
||||
*/
|
||||
|
||||
@@ -381,7 +381,7 @@ export type OpportunityGroupByOutputType = {
|
||||
name: string
|
||||
notes: string | null
|
||||
oppNarrative: string | null
|
||||
typeId: number
|
||||
typeId: number | null
|
||||
stageId: number | null
|
||||
statusId: number | null
|
||||
taxCodeId: number | null
|
||||
@@ -438,7 +438,7 @@ export type OpportunityWhereInput = {
|
||||
name?: Prisma.StringFilter<"Opportunity"> | string
|
||||
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||
typeId?: Prisma.IntFilter<"Opportunity"> | number
|
||||
typeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
@@ -465,7 +465,7 @@ export type OpportunityWhereInput = {
|
||||
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
||||
type?: Prisma.XOR<Prisma.OpportunityTypeScalarRelationFilter, Prisma.OpportunityTypeWhereInput>
|
||||
type?: Prisma.XOR<Prisma.OpportunityTypeNullableScalarRelationFilter, Prisma.OpportunityTypeWhereInput> | null
|
||||
stage?: Prisma.XOR<Prisma.OpportunityStageNullableScalarRelationFilter, Prisma.OpportunityStageWhereInput> | null
|
||||
status?: Prisma.XOR<Prisma.OpportunityStatusNullableScalarRelationFilter, Prisma.OpportunityStatusWhereInput> | null
|
||||
taxCode?: Prisma.XOR<Prisma.TaxCodeNullableScalarRelationFilter, Prisma.TaxCodeWhereInput> | null
|
||||
@@ -485,7 +485,7 @@ export type OpportunityOrderByWithRelationInput = {
|
||||
name?: Prisma.SortOrder
|
||||
notes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
oppNarrative?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
typeId?: Prisma.SortOrder
|
||||
typeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
stageId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
statusId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
taxCodeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
@@ -535,7 +535,7 @@ export type OpportunityWhereUniqueInput = Prisma.AtLeast<{
|
||||
name?: Prisma.StringFilter<"Opportunity"> | string
|
||||
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||
typeId?: Prisma.IntFilter<"Opportunity"> | number
|
||||
typeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
@@ -562,7 +562,7 @@ export type OpportunityWhereUniqueInput = Prisma.AtLeast<{
|
||||
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesListRelationFilter
|
||||
type?: Prisma.XOR<Prisma.OpportunityTypeScalarRelationFilter, Prisma.OpportunityTypeWhereInput>
|
||||
type?: Prisma.XOR<Prisma.OpportunityTypeNullableScalarRelationFilter, Prisma.OpportunityTypeWhereInput> | null
|
||||
stage?: Prisma.XOR<Prisma.OpportunityStageNullableScalarRelationFilter, Prisma.OpportunityStageWhereInput> | null
|
||||
status?: Prisma.XOR<Prisma.OpportunityStatusNullableScalarRelationFilter, Prisma.OpportunityStatusWhereInput> | null
|
||||
taxCode?: Prisma.XOR<Prisma.TaxCodeNullableScalarRelationFilter, Prisma.TaxCodeWhereInput> | null
|
||||
@@ -582,7 +582,7 @@ export type OpportunityOrderByWithAggregationInput = {
|
||||
name?: Prisma.SortOrder
|
||||
notes?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
oppNarrative?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
typeId?: Prisma.SortOrder
|
||||
typeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
stageId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
statusId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
taxCodeId?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
@@ -624,7 +624,7 @@ export type OpportunityScalarWhereWithAggregatesInput = {
|
||||
name?: Prisma.StringWithAggregatesFilter<"Opportunity"> | string
|
||||
notes?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
|
||||
oppNarrative?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
|
||||
typeId?: Prisma.IntWithAggregatesFilter<"Opportunity"> | number
|
||||
typeId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||
stageId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||
statusId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||
taxCodeId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
|
||||
@@ -674,7 +674,7 @@ export type OpportunityCreateInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -694,7 +694,7 @@ export type OpportunityUncheckedCreateInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -746,7 +746,7 @@ export type OpportunityUpdateInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -766,7 +766,7 @@ export type OpportunityUncheckedUpdateInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -802,7 +802,7 @@ export type OpportunityCreateManyInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -859,7 +859,7 @@ export type OpportunityUncheckedUpdateManyInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -1572,7 +1572,7 @@ export type OpportunityCreateWithoutPrimarySalesRepInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -1591,7 +1591,7 @@ export type OpportunityUncheckedCreateWithoutPrimarySalesRepInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -1652,7 +1652,7 @@ export type OpportunityCreateWithoutSecondarySalesRepInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -1671,7 +1671,7 @@ export type OpportunityUncheckedCreateWithoutSecondarySalesRepInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -1735,7 +1735,7 @@ export type OpportunityScalarWhereInput = {
|
||||
name?: Prisma.StringFilter<"Opportunity"> | string
|
||||
notes?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||
oppNarrative?: Prisma.StringNullableFilter<"Opportunity"> | string | null
|
||||
typeId?: Prisma.IntFilter<"Opportunity"> | number
|
||||
typeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
stageId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
statusId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
taxCodeId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
|
||||
@@ -1801,7 +1801,7 @@ export type OpportunityCreateWithoutLocationInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -1820,7 +1820,7 @@ export type OpportunityUncheckedCreateWithoutLocationInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -1897,7 +1897,7 @@ export type OpportunityCreateWithoutDepartmentInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -1916,7 +1916,7 @@ export type OpportunityUncheckedCreateWithoutDepartmentInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -1993,7 +1993,7 @@ export type OpportunityCreateWithoutCompanyInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2012,7 +2012,7 @@ export type OpportunityUncheckedCreateWithoutCompanyInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -2089,7 +2089,7 @@ export type OpportunityCreateWithoutSiteInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2108,7 +2108,7 @@ export type OpportunityUncheckedCreateWithoutSiteInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -2185,7 +2185,7 @@ export type OpportunityCreateWithoutContactInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2204,7 +2204,7 @@ export type OpportunityUncheckedCreateWithoutContactInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -2281,7 +2281,7 @@ export type OpportunityCreateWithoutProductsInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2300,7 +2300,7 @@ export type OpportunityUncheckedCreateWithoutProductsInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -2367,7 +2367,7 @@ export type OpportunityUpdateWithoutProductsInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -2386,7 +2386,7 @@ export type OpportunityUncheckedUpdateWithoutProductsInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -2437,7 +2437,7 @@ export type OpportunityCreateWithoutStageInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2456,7 +2456,7 @@ export type OpportunityUncheckedCreateWithoutStageInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
interest?: $Enums.OpportunityInterest | null
|
||||
@@ -2629,7 +2629,7 @@ export type OpportunityCreateWithoutStatusInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2648,7 +2648,7 @@ export type OpportunityUncheckedCreateWithoutStatusInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
taxCodeId?: number | null
|
||||
interest?: $Enums.OpportunityInterest | null
|
||||
@@ -2724,7 +2724,7 @@ export type OpportunityCreateWithoutGeneratedQuotesInput = {
|
||||
eneteredBy: string
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
taxCode?: Prisma.TaxCodeCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2744,7 +2744,7 @@ export type OpportunityUncheckedCreateWithoutGeneratedQuotesInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -2810,7 +2810,7 @@ export type OpportunityUpdateWithoutGeneratedQuotesInput = {
|
||||
eneteredBy?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -2830,7 +2830,7 @@ export type OpportunityUncheckedUpdateWithoutGeneratedQuotesInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -2881,7 +2881,7 @@ export type OpportunityCreateWithoutTaxCodeInput = {
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesCreateNestedManyWithoutOpportunityInput
|
||||
type: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
type?: Prisma.OpportunityTypeCreateNestedOneWithoutOpportunitiesInput
|
||||
stage?: Prisma.OpportunityStageCreateNestedOneWithoutOpportunitiesInput
|
||||
status?: Prisma.OpportunityStatusCreateNestedOneWithoutOpportunitiesInput
|
||||
primarySalesRep?: Prisma.UserCreateNestedOneWithoutOpportunitiesInput
|
||||
@@ -2900,7 +2900,7 @@ export type OpportunityUncheckedCreateWithoutTaxCodeInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
interest?: $Enums.OpportunityInterest | null
|
||||
@@ -2961,7 +2961,7 @@ export type OpportunityCreateManyPrimarySalesRepInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -2994,7 +2994,7 @@ export type OpportunityCreateManySecondarySalesRepInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -3043,7 +3043,7 @@ export type OpportunityUpdateWithoutPrimarySalesRepInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3062,7 +3062,7 @@ export type OpportunityUncheckedUpdateWithoutPrimarySalesRepInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3097,7 +3097,7 @@ export type OpportunityUncheckedUpdateManyWithoutPrimarySalesRepInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3146,7 +3146,7 @@ export type OpportunityUpdateWithoutSecondarySalesRepInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3165,7 +3165,7 @@ export type OpportunityUncheckedUpdateWithoutSecondarySalesRepInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3200,7 +3200,7 @@ export type OpportunityUncheckedUpdateManyWithoutSecondarySalesRepInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3233,7 +3233,7 @@ export type OpportunityCreateManyLocationInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -3282,7 +3282,7 @@ export type OpportunityUpdateWithoutLocationInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3301,7 +3301,7 @@ export type OpportunityUncheckedUpdateWithoutLocationInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3336,7 +3336,7 @@ export type OpportunityUncheckedUpdateManyWithoutLocationInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3369,7 +3369,7 @@ export type OpportunityCreateManyDepartmentInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -3418,7 +3418,7 @@ export type OpportunityUpdateWithoutDepartmentInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3437,7 +3437,7 @@ export type OpportunityUncheckedUpdateWithoutDepartmentInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3472,7 +3472,7 @@ export type OpportunityUncheckedUpdateManyWithoutDepartmentInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3505,7 +3505,7 @@ export type OpportunityCreateManyCompanyInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -3554,7 +3554,7 @@ export type OpportunityUpdateWithoutCompanyInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3573,7 +3573,7 @@ export type OpportunityUncheckedUpdateWithoutCompanyInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3608,7 +3608,7 @@ export type OpportunityUncheckedUpdateManyWithoutCompanyInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3641,7 +3641,7 @@ export type OpportunityCreateManySiteInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -3690,7 +3690,7 @@ export type OpportunityUpdateWithoutSiteInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3709,7 +3709,7 @@ export type OpportunityUncheckedUpdateWithoutSiteInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3744,7 +3744,7 @@ export type OpportunityUncheckedUpdateManyWithoutSiteInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3777,7 +3777,7 @@ export type OpportunityCreateManyContactInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
@@ -3826,7 +3826,7 @@ export type OpportunityUpdateWithoutContactInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3845,7 +3845,7 @@ export type OpportunityUncheckedUpdateWithoutContactInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3880,7 +3880,7 @@ export type OpportunityUncheckedUpdateManyWithoutContactInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
@@ -3913,7 +3913,7 @@ export type OpportunityCreateManyStageInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
statusId?: number | null
|
||||
taxCodeId?: number | null
|
||||
interest?: $Enums.OpportunityInterest | null
|
||||
@@ -3962,7 +3962,7 @@ export type OpportunityUpdateWithoutStageInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -3981,7 +3981,7 @@ export type OpportunityUncheckedUpdateWithoutStageInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||
@@ -4016,7 +4016,7 @@ export type OpportunityUncheckedUpdateManyWithoutStageInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||
@@ -4185,7 +4185,7 @@ export type OpportunityCreateManyStatusInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
taxCodeId?: number | null
|
||||
interest?: $Enums.OpportunityInterest | null
|
||||
@@ -4234,7 +4234,7 @@ export type OpportunityUpdateWithoutStatusInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
taxCode?: Prisma.TaxCodeUpdateOneWithoutOpportunitiesNestedInput
|
||||
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -4253,7 +4253,7 @@ export type OpportunityUncheckedUpdateWithoutStatusInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||
@@ -4288,7 +4288,7 @@ export type OpportunityUncheckedUpdateManyWithoutStatusInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
taxCodeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||
@@ -4321,7 +4321,7 @@ export type OpportunityCreateManyTaxCodeInput = {
|
||||
name: string
|
||||
notes?: string | null
|
||||
oppNarrative?: string | null
|
||||
typeId: number
|
||||
typeId?: number | null
|
||||
stageId?: number | null
|
||||
statusId?: number | null
|
||||
interest?: $Enums.OpportunityInterest | null
|
||||
@@ -4370,7 +4370,7 @@ export type OpportunityUpdateWithoutTaxCodeInput = {
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
generatedQuotes?: Prisma.GeneratedQuotesUpdateManyWithoutOpportunityNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneRequiredWithoutOpportunitiesNestedInput
|
||||
type?: Prisma.OpportunityTypeUpdateOneWithoutOpportunitiesNestedInput
|
||||
stage?: Prisma.OpportunityStageUpdateOneWithoutOpportunitiesNestedInput
|
||||
status?: Prisma.OpportunityStatusUpdateOneWithoutOpportunitiesNestedInput
|
||||
primarySalesRep?: Prisma.UserUpdateOneWithoutOpportunitiesNestedInput
|
||||
@@ -4389,7 +4389,7 @@ export type OpportunityUncheckedUpdateWithoutTaxCodeInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||
@@ -4424,7 +4424,7 @@ export type OpportunityUncheckedUpdateManyWithoutTaxCodeInput = {
|
||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
notes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
oppNarrative?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
typeId?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
typeId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
stageId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
statusId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
|
||||
interest?: Prisma.NullableEnumOpportunityInterestFieldUpdateOperationsInput | $Enums.OpportunityInterest | null
|
||||
@@ -4524,7 +4524,7 @@ export type OpportunitySelect<ExtArgs extends runtime.Types.Extensions.InternalA
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
generatedQuotes?: boolean | Prisma.Opportunity$generatedQuotesArgs<ExtArgs>
|
||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
||||
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||
@@ -4571,7 +4571,7 @@ export type OpportunitySelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
|
||||
eneteredBy?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
||||
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||
@@ -4616,7 +4616,7 @@ export type OpportunitySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
|
||||
eneteredBy?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
||||
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||
@@ -4666,7 +4666,7 @@ export type OpportunitySelectScalar = {
|
||||
export type OpportunityOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "uid" | "name" | "notes" | "oppNarrative" | "typeId" | "stageId" | "statusId" | "taxCodeId" | "interest" | "probability" | "source" | "primarySalesRepId" | "secondarySalesRepId" | "companyId" | "contactId" | "siteId" | "customerPO" | "locationId" | "departmentId" | "expectedCloseDate" | "pipelineChangeDate" | "dateBecameLead" | "closedDate" | "closedFlag" | "closedById" | "productSequence" | "updatedBy" | "eneteredBy" | "createdAt" | "updatedAt", ExtArgs["result"]["opportunity"]>
|
||||
export type OpportunityInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
generatedQuotes?: boolean | Prisma.Opportunity$generatedQuotesArgs<ExtArgs>
|
||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
||||
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||
@@ -4681,7 +4681,7 @@ export type OpportunityInclude<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
_count?: boolean | Prisma.OpportunityCountOutputTypeDefaultArgs<ExtArgs>
|
||||
}
|
||||
export type OpportunityIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
||||
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||
@@ -4694,7 +4694,7 @@ export type OpportunityIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.
|
||||
department?: boolean | Prisma.Opportunity$departmentArgs<ExtArgs>
|
||||
}
|
||||
export type OpportunityIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
type?: boolean | Prisma.OpportunityTypeDefaultArgs<ExtArgs>
|
||||
type?: boolean | Prisma.Opportunity$typeArgs<ExtArgs>
|
||||
stage?: boolean | Prisma.Opportunity$stageArgs<ExtArgs>
|
||||
status?: boolean | Prisma.Opportunity$statusArgs<ExtArgs>
|
||||
taxCode?: boolean | Prisma.Opportunity$taxCodeArgs<ExtArgs>
|
||||
@@ -4711,7 +4711,7 @@ export type $OpportunityPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
||||
name: "Opportunity"
|
||||
objects: {
|
||||
generatedQuotes: Prisma.$GeneratedQuotesPayload<ExtArgs>[]
|
||||
type: Prisma.$OpportunityTypePayload<ExtArgs>
|
||||
type: Prisma.$OpportunityTypePayload<ExtArgs> | null
|
||||
stage: Prisma.$OpportunityStagePayload<ExtArgs> | null
|
||||
status: Prisma.$OpportunityStatusPayload<ExtArgs> | null
|
||||
taxCode: Prisma.$TaxCodePayload<ExtArgs> | null
|
||||
@@ -4730,7 +4730,7 @@ export type $OpportunityPayload<ExtArgs extends runtime.Types.Extensions.Interna
|
||||
name: string
|
||||
notes: string | null
|
||||
oppNarrative: string | null
|
||||
typeId: number
|
||||
typeId: number | null
|
||||
stageId: number | null
|
||||
statusId: number | null
|
||||
taxCodeId: number | null
|
||||
@@ -5151,7 +5151,7 @@ readonly fields: OpportunityFieldRefs;
|
||||
export interface Prisma__OpportunityClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||
generatedQuotes<T extends Prisma.Opportunity$generatedQuotesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$generatedQuotesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$GeneratedQuotesPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||
type<T extends Prisma.OpportunityTypeDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.OpportunityTypeDefaultArgs<ExtArgs>>): Prisma.Prisma__OpportunityTypeClient<runtime.Types.Result.GetResult<Prisma.$OpportunityTypePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||
type<T extends Prisma.Opportunity$typeArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$typeArgs<ExtArgs>>): Prisma.Prisma__OpportunityTypeClient<runtime.Types.Result.GetResult<Prisma.$OpportunityTypePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
stage<T extends Prisma.Opportunity$stageArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$stageArgs<ExtArgs>>): Prisma.Prisma__OpportunityStageClient<runtime.Types.Result.GetResult<Prisma.$OpportunityStagePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
status<T extends Prisma.Opportunity$statusArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$statusArgs<ExtArgs>>): Prisma.Prisma__OpportunityStatusClient<runtime.Types.Result.GetResult<Prisma.$OpportunityStatusPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
taxCode<T extends Prisma.Opportunity$taxCodeArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Opportunity$taxCodeArgs<ExtArgs>>): Prisma.Prisma__TaxCodeClient<runtime.Types.Result.GetResult<Prisma.$TaxCodePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||
@@ -5647,6 +5647,25 @@ export type Opportunity$generatedQuotesArgs<ExtArgs extends runtime.Types.Extens
|
||||
distinct?: Prisma.GeneratedQuotesScalarFieldEnum | Prisma.GeneratedQuotesScalarFieldEnum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Opportunity.type
|
||||
*/
|
||||
export type Opportunity$typeArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
/**
|
||||
* Select specific fields to fetch from the OpportunityType
|
||||
*/
|
||||
select?: Prisma.OpportunityTypeSelect<ExtArgs> | null
|
||||
/**
|
||||
* Omit specific fields from the OpportunityType
|
||||
*/
|
||||
omit?: Prisma.OpportunityTypeOmit<ExtArgs> | null
|
||||
/**
|
||||
* Choose, which related nodes to fetch as well
|
||||
*/
|
||||
include?: Prisma.OpportunityTypeInclude<ExtArgs> | null
|
||||
where?: Prisma.OpportunityTypeWhereInput
|
||||
}
|
||||
|
||||
/**
|
||||
* Opportunity.stage
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
|
||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
||||
|
||||
Commands marked with * may be preceded by a number, _N.
|
||||
Notes in parentheses indicate the behavior if _N is given.
|
||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
||||
|
||||
h H Display this help.
|
||||
q :q Q :Q ZZ Exit.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMOOVVIINNGG
|
||||
|
||||
e ^E j ^N CR * Forward one line (or _N lines).
|
||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
||||
b ^B ESC-v * Backward one window (or _N lines).
|
||||
z * Forward one window (and set window to _N).
|
||||
w * Backward one window (and set window to _N).
|
||||
ESC-SPACE * Forward one window, but don't stop at end-of-file.
|
||||
d ^D * Forward one half-window (and set half-window to _N).
|
||||
u ^U * Backward one half-window (and set half-window to _N).
|
||||
ESC-) RightArrow * Right one half screen width (or _N positions).
|
||||
ESC-( LeftArrow * Left one half screen width (or _N positions).
|
||||
ESC-} ^RightArrow Right to last column displayed.
|
||||
ESC-{ ^LeftArrow Left to first column.
|
||||
F Forward forever; like "tail -f".
|
||||
ESC-F Like F but stop when search pattern is found.
|
||||
r ^R ^L Repaint screen.
|
||||
R Repaint screen, discarding buffered input.
|
||||
---------------------------------------------------
|
||||
Default "window" is the screen height.
|
||||
Default "half-window" is half of the screen height.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
SSEEAARRCCHHIINNGG
|
||||
|
||||
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
|
||||
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
|
||||
n * Repeat previous search (for _N-th occurrence).
|
||||
N * Repeat previous search in reverse direction.
|
||||
ESC-n * Repeat previous search, spanning files.
|
||||
ESC-N * Repeat previous search, reverse dir. & spanning files.
|
||||
ESC-u Undo (toggle) search highlighting.
|
||||
ESC-U Clear search highlighting.
|
||||
&_p_a_t_t_e_r_n * Display only matching lines.
|
||||
---------------------------------------------------
|
||||
A search pattern may begin with one or more of:
|
||||
^N or ! Search for NON-matching lines.
|
||||
^E or * Search multiple files (pass thru END OF FILE).
|
||||
^F or @ Start search at FIRST file (for /) or last file (for ?).
|
||||
^K Highlight matches, but don't move (KEEP position).
|
||||
^R Don't use REGULAR EXPRESSIONS.
|
||||
^W WRAP search if no match found.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
JJUUMMPPIINNGG
|
||||
|
||||
g < ESC-< * Go to first line in file (or line _N).
|
||||
G > ESC-> * Go to last line in file (or line _N).
|
||||
p % * Go to beginning of file (or _N percent into file).
|
||||
t * Go to the (_N-th) next tag.
|
||||
T * Go to the (_N-th) previous tag.
|
||||
{ ( [ * Find close bracket } ) ].
|
||||
} ) ] * Find open bracket { ( [.
|
||||
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
|
||||
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
|
||||
---------------------------------------------------
|
||||
Each "find close bracket" command goes forward to the close bracket
|
||||
matching the (_N-th) open bracket in the top line.
|
||||
Each "find open bracket" command goes backward to the open bracket
|
||||
matching the (_N-th) close bracket in the bottom line.
|
||||
|
||||
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
|
||||
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
|
||||
'_<_l_e_t_t_e_r_> Go to a previously marked position.
|
||||
'' Go to the previous position.
|
||||
^X^X Same as '.
|
||||
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
|
||||
---------------------------------------------------
|
||||
A mark is any upper-case or lower-case letter.
|
||||
Certain marks are predefined:
|
||||
^ means beginning of the file
|
||||
$ means end of the file
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CCHHAANNGGIINNGG FFIILLEESS
|
||||
|
||||
:e [_f_i_l_e] Examine a new file.
|
||||
^X^V Same as :e.
|
||||
:n * Examine the (_N-th) next file from the command line.
|
||||
:p * Examine the (_N-th) previous file from the command line.
|
||||
:x * Examine the first (or _N-th) file from the command line.
|
||||
:d Delete the current file from the command line list.
|
||||
= ^G :f Print current file name.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
|
||||
|
||||
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
|
||||
--_<_n_a_m_e_> Toggle a command line option, by name.
|
||||
__<_f_l_a_g_> Display the setting of a command line option.
|
||||
___<_n_a_m_e_> Display the setting of an option, by name.
|
||||
+_c_m_d Execute the less cmd each time a new file is examined.
|
||||
|
||||
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|
||||
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
|
||||
s _f_i_l_e Save input to a file.
|
||||
v Edit the current file with $VISUAL or $EDITOR.
|
||||
V Print version number of "less".
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
OOPPTTIIOONNSS
|
||||
|
||||
Most options may be changed either on the command line,
|
||||
or from within less by using the - or -- command.
|
||||
Options may be given in one of two forms: either a single
|
||||
character preceded by a -, or a name preceded by --.
|
||||
|
||||
-? ........ --help
|
||||
Display help (from command line).
|
||||
-a ........ --search-skip-screen
|
||||
Search skips current screen.
|
||||
-A ........ --SEARCH-SKIP-SCREEN
|
||||
Search starts just after target line.
|
||||
-b [_N] .... --buffers=[_N]
|
||||
Number of buffers.
|
||||
-B ........ --auto-buffers
|
||||
Don't automatically allocate buffers for pipes.
|
||||
-c ........ --clear-screen
|
||||
Repaint by clearing rather than scrolling.
|
||||
-d ........ --dumb
|
||||
Dumb terminal.
|
||||
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
|
||||
Set screen colors.
|
||||
-e -E .... --quit-at-eof --QUIT-AT-EOF
|
||||
Quit at end of file.
|
||||
-f ........ --force
|
||||
Force open non-regular files.
|
||||
-F ........ --quit-if-one-screen
|
||||
Quit if entire file fits on first screen.
|
||||
-g ........ --hilite-search
|
||||
Highlight only last match for searches.
|
||||
-G ........ --HILITE-SEARCH
|
||||
Don't highlight any matches for searches.
|
||||
-h [_N] .... --max-back-scroll=[_N]
|
||||
Backward scroll limit.
|
||||
-i ........ --ignore-case
|
||||
Ignore case in searches that do not contain uppercase.
|
||||
-I ........ --IGNORE-CASE
|
||||
Ignore case in all searches.
|
||||
-j [_N] .... --jump-target=[_N]
|
||||
Screen position of target lines.
|
||||
-J ........ --status-column
|
||||
Display a status column at left edge of screen.
|
||||
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
|
||||
Use a lesskey file.
|
||||
-K ........ --quit-on-intr
|
||||
Exit less in response to ctrl-C.
|
||||
-L ........ --no-lessopen
|
||||
Ignore the LESSOPEN environment variable.
|
||||
-m -M .... --long-prompt --LONG-PROMPT
|
||||
Set prompt style.
|
||||
-n -N .... --line-numbers --LINE-NUMBERS
|
||||
Don't use line numbers.
|
||||
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
|
||||
Copy to log file (standard input only).
|
||||
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
|
||||
Copy to log file (unconditionally overwrite).
|
||||
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
|
||||
Start at pattern (from command line).
|
||||
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
|
||||
Define new prompt.
|
||||
-q -Q .... --quiet --QUIET --silent --SILENT
|
||||
Quiet the terminal bell.
|
||||
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
|
||||
Output "raw" control characters.
|
||||
-s ........ --squeeze-blank-lines
|
||||
Squeeze multiple blank lines.
|
||||
-S ........ --chop-long-lines
|
||||
Chop (truncate) long lines rather than wrapping.
|
||||
-t [_t_a_g] .. --tag=[_t_a_g]
|
||||
Find a tag.
|
||||
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
|
||||
Use an alternate tags file.
|
||||
-u -U .... --underline-special --UNDERLINE-SPECIAL
|
||||
Change handling of backspaces.
|
||||
-V ........ --version
|
||||
Display the version number of "less".
|
||||
-w ........ --hilite-unread
|
||||
Highlight first new line after forward-screen.
|
||||
-W ........ --HILITE-UNREAD
|
||||
Highlight first new line after any forward movement.
|
||||
-x [_N[,...]] --tabs=[_N[,...]]
|
||||
Set tab stops.
|
||||
-X ........ --no-init
|
||||
Don't use termcap init/deinit strings.
|
||||
-y [_N] .... --max-forw-scroll=[_N]
|
||||
Forward scroll limit.
|
||||
-z [_N] .... --window=[_N]
|
||||
Set size of window.
|
||||
-" [_c[_c]] . --quotes=[_c[_c]]
|
||||
Set shell quote characters.
|
||||
-~ ........ --tilde
|
||||
Don't display tildes after end of file.
|
||||
-# [_N] .... --shift=[_N]
|
||||
Set horizontal scroll amount (0 = one half screen width).
|
||||
--file-size
|
||||
Automatically determine the size of the input file.
|
||||
--follow-name
|
||||
The F command changes files if the input file is renamed.
|
||||
--incsearch
|
||||
Search file as each pattern character is typed in.
|
||||
--line-num-width=N
|
||||
Set the width of the -N line number field to N characters.
|
||||
--mouse
|
||||
Enable mouse input.
|
||||
--no-keypad
|
||||
Don't send termcap keypad init/deinit strings.
|
||||
--no-histdups
|
||||
Remove duplicates from command history.
|
||||
--rscroll=C
|
||||
Set the character used to mark truncated lines.
|
||||
--save-marks
|
||||
Retain marks across invocations of less.
|
||||
--status-col-width=N
|
||||
Set the width of the -J status column to N characters.
|
||||
--use-backslash
|
||||
Subsequent options use backslash as escape char.
|
||||
--use-color
|
||||
Enables colored text.
|
||||
--wheel-lines=N
|
||||
Each click of the mouse wheel moves N lines.
|
||||
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
LLIINNEE EEDDIITTIINNGG
|
||||
|
||||
These keys can be used to edit text being entered
|
||||
on the "command line" at the bottom of the screen.
|
||||
|
||||
RightArrow ..................... ESC-l ... Move cursor right one character.
|
||||
LeftArrow ...................... ESC-h ... Move cursor left one character.
|
||||
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
|
||||
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
|
||||
HOME ........................... ESC-0 ... Move cursor to start of line.
|
||||
END ............................ ESC-$ ... Move cursor to end of line.
|
||||
BACKSPACE ................................ Delete char to left of cursor.
|
||||
DELETE ......................... ESC-x ... Delete char under cursor.
|
||||
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
|
||||
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
|
||||
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
|
||||
UpArrow ........................ ESC-k ... Retrieve previous command line.
|
||||
DownArrow ...................... ESC-j ... Retrieve next command line.
|
||||
TAB ...................................... Complete filename & cycle.
|
||||
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
|
||||
ctrl-L ................................... Complete filename, list all.
|
||||
@@ -1,11 +1,10 @@
|
||||
import { defineConfig, env } from 'prisma/config'
|
||||
|
||||
export default defineConfig({
|
||||
export default {
|
||||
schema: 'prisma/schema.prisma',
|
||||
migrations: {
|
||||
path: 'prisma/migrations',
|
||||
},
|
||||
datasource: {
|
||||
url: env('DATABASE_URL'),
|
||||
url: process.env.DATABASE_URL,
|
||||
shadowDatabaseUrl: process.env.SHADOW_DATABASE_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Fix schema drift: the Opportunity.typeId column was added as NOT NULL but the
|
||||
-- Prisma schema declares it as Int? (nullable). Drop the NOT NULL constraint so
|
||||
-- the DB matches the schema and so that creating an opportunity without a type
|
||||
-- (e.g. when CW uses no default type) no longer fails with P2011.
|
||||
ALTER TABLE "Opportunity" ALTER COLUMN "typeId" DROP NOT NULL;
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Opportunity" DROP CONSTRAINT "Opportunity_typeId_fkey";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Activity" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"startTime" TIMESTAMP(3) NOT NULL,
|
||||
"endTime" TIMESTAMP(3) NOT NULL,
|
||||
"assignToId" TEXT NOT NULL,
|
||||
"assignedById" TEXT NOT NULL,
|
||||
"enteredBy" TEXT NOT NULL,
|
||||
"automated" BOOLEAN NOT NULL DEFAULT false,
|
||||
"closedFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"notifyCompleteFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"notificationSentFlat" BOOLEAN NOT NULL DEFAULT false,
|
||||
"opportunityId" TEXT,
|
||||
"serviceTicketId" TEXT,
|
||||
"contactId" INTEGER,
|
||||
"companyId" INTEGER,
|
||||
"activityTypeId" INTEGER,
|
||||
"activityStatusId" INTEGER,
|
||||
"createdById" TEXT,
|
||||
"updatedById" TEXT,
|
||||
"closedById" TEXT,
|
||||
"closedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Activity_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ActivityNotes" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"notes" TEXT NOT NULL DEFAULT '',
|
||||
"activityId" INTEGER,
|
||||
"internalAnalysisFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"enteredById" TEXT,
|
||||
"updatedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ActivityNotes_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ActivityType" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"name" VARCHAR(15) NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"inactiveFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"historyFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"defaultFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"importFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"emailFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"memoFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"pointsValue" INTEGER,
|
||||
"updatedById" TEXT,
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ActivityType_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ActivityStatus" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"closedFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"inactiveFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"defaultFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"spawnFollowupFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedById" TEXT,
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ActivityStatus_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TimeEntry" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"memberId" TEXT,
|
||||
"serviceTicketId" INTEGER,
|
||||
"activityId" INTEGER,
|
||||
"projectId" INTEGER,
|
||||
"chargeCodeId" INTEGER,
|
||||
"companyId" INTEGER NOT NULL,
|
||||
"statusId" INTEGER,
|
||||
"locationId" INTEGER,
|
||||
"contactId" INTEGER,
|
||||
"dateStart" TIMESTAMP(3),
|
||||
"timeStart" TIMESTAMP(3),
|
||||
"timeEnd" TIMESTAMP(3),
|
||||
"notes" TEXT,
|
||||
"notesMd" TEXT,
|
||||
"internalNote" TEXT,
|
||||
"billableHours" DOUBLE PRECISION,
|
||||
"actualHours" DOUBLE PRECISION,
|
||||
"invoicedHours" DOUBLE PRECISION,
|
||||
"deductedHours" DOUBLE PRECISION,
|
||||
"hourlyRate" DOUBLE PRECISION,
|
||||
"effectiveRate" DOUBLE PRECISION,
|
||||
"issueFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"mergedFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"invoiceFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"billableFlag" BOOLEAN NOT NULL DEFAULT true,
|
||||
"documentFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"teProblemFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"teResolutionFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"teInternalAnalysisFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"chargeToRecId" INTEGER,
|
||||
"chargeToType" VARCHAR(13),
|
||||
"createdById" TEXT,
|
||||
"updatedById" TEXT,
|
||||
"originalAuthorId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TimeEntry_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TimeEntryStatus" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"statusId" INTEGER NOT NULL,
|
||||
"description" VARCHAR(50),
|
||||
"action" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TimeEntryStatus_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TimeEntryChargeCode" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"chargeCodeId" INTEGER NOT NULL,
|
||||
"description" VARCHAR(50),
|
||||
"expenseFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"timeFlag" BOOLEAN NOT NULL DEFAULT true,
|
||||
"billableFlag" BOOLEAN NOT NULL DEFAULT true,
|
||||
"invoiceFlag" BOOLEAN NOT NULL DEFAULT true,
|
||||
"updatedById" TEXT,
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TimeEntryChargeCode_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TimeActivityClass" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"description" VARCHAR(50),
|
||||
"hourlyRate" DOUBLE PRECISION,
|
||||
"inactiveFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"taxExemptFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdById" TEXT,
|
||||
"updatedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TimeActivityClass_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TimeActivityType" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"description" VARCHAR(50),
|
||||
"minHours" DOUBLE PRECISION,
|
||||
"maxHours" DOUBLE PRECISION,
|
||||
"rate" DOUBLE PRECISION DEFAULT 1,
|
||||
"costMultiplier" DOUBLE PRECISION NOT NULL DEFAULT 1,
|
||||
"inactiveFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"invoiceFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"billableFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"utilizationFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"defaultFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"multiplierFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdById" TEXT,
|
||||
"updatedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TimeActivityType_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CwMemberType" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uid" TEXT NOT NULL,
|
||||
"description" VARCHAR(30),
|
||||
"inactiveFlag" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedById" TEXT,
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CwMemberType_pkey" PRIMARY KEY ("uid")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Activity_id_key" ON "Activity"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ActivityNotes_id_key" ON "ActivityNotes"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ActivityNotes_activityId_key" ON "ActivityNotes"("activityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ActivityType_id_key" ON "ActivityType"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ActivityStatus_id_key" ON "ActivityStatus"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeEntry_id_key" ON "TimeEntry"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeEntryStatus_id_key" ON "TimeEntryStatus"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeEntryStatus_statusId_key" ON "TimeEntryStatus"("statusId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeEntryChargeCode_id_key" ON "TimeEntryChargeCode"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeEntryChargeCode_chargeCodeId_key" ON "TimeEntryChargeCode"("chargeCodeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeActivityClass_id_key" ON "TimeActivityClass"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TimeActivityType_id_key" ON "TimeActivityType"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CwMemberType_id_key" ON "CwMemberType"("id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "OpportunityType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_activityTypeId_fkey" FOREIGN KEY ("activityTypeId") REFERENCES "ActivityType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_activityStatusId_fkey" FOREIGN KEY ("activityStatusId") REFERENCES "ActivityStatus"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ActivityNotes" ADD CONSTRAINT "ActivityNotes_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "CorporateLocation"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "TimeEntryStatus"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_chargeCodeId_fkey" FOREIGN KEY ("chargeCodeId") REFERENCES "TimeEntryChargeCode"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_serviceTicketId_fkey" FOREIGN KEY ("serviceTicketId") REFERENCES "ServiceTicket"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TimeEntry" ADD CONSTRAINT "TimeEntry_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+334
-46
@@ -124,10 +124,10 @@ enum SyncJobStatus {
|
||||
}
|
||||
|
||||
model SyncJobRun {
|
||||
id String @id @default(uuid())
|
||||
jobType SyncJobType
|
||||
status SyncJobStatus @default(QUEUED)
|
||||
triggeredBy String @default("system")
|
||||
id String @id @default(uuid())
|
||||
jobType SyncJobType
|
||||
status SyncJobStatus @default(QUEUED)
|
||||
triggeredBy String @default("system")
|
||||
|
||||
startedAt DateTime?
|
||||
completedAt DateTime?
|
||||
@@ -250,6 +250,7 @@ model CorporateLocation {
|
||||
opportunities Opportunity[]
|
||||
serviceBoards ServiceTicketBoard[]
|
||||
productDataRecords ProductData[]
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -307,6 +308,9 @@ model Company {
|
||||
credentials Credential[]
|
||||
unifiSites UnifiSite[]
|
||||
opportunities Opportunity[]
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
activities Activity[]
|
||||
|
||||
deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier])
|
||||
enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier])
|
||||
@@ -384,12 +388,15 @@ model Contact {
|
||||
memberId Int?
|
||||
companyId Int?
|
||||
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
opportunities Opportunity[]
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
opportunities Opportunity[]
|
||||
serviceTickets ServiceTicket[]
|
||||
activities Activity[]
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CatalogItemType {
|
||||
@@ -606,7 +613,7 @@ model ProductData {
|
||||
qty Float @default(1)
|
||||
internalNote String?
|
||||
shortDescription String?
|
||||
description String?
|
||||
description String?
|
||||
sequenceNumber Int? // This is the sequence number of the product on the ticket, which may be different from the sequence number of the product in the catalog, and is important for maintaining the order of products on the ticket as they were added.
|
||||
|
||||
procurementNotes String?
|
||||
@@ -692,7 +699,8 @@ model ServiceTicket {
|
||||
|
||||
// ------ Billing and invoicing fields ------
|
||||
|
||||
products ProductData[] // The products used on this ticket, which may be important for billing and invoicing, as well as reporting and analytics.
|
||||
products ProductData[] // The products used on this ticket, which may be important for billing and invoicing, as well as reporting and analytics.
|
||||
timeEntries TimeEntry[] // The time entries logged against this ticket.
|
||||
|
||||
poNumber String?
|
||||
billCompleteFlag Boolean @default(false) // Bill after ticket is closed, not allowing any billing while open.
|
||||
@@ -739,12 +747,12 @@ model ServiceTicket {
|
||||
billingAddressId Int?
|
||||
|
||||
severity ServiceTicketSeverity @relation(fields: [severityId], references: [id])
|
||||
impact ServiceTicketImpact @relation(fields: [impactId], references: [id])
|
||||
priority ServiceTicketPriority @relation(fields: [priorityId], references: [id])
|
||||
source ServiceTicketSource @relation(fields: [sourceId], references: [id])
|
||||
location ServiceTicketLocation @relation(fields: [locationId], references: [id])
|
||||
serviceTicketBoard ServiceTicketBoard? @relation(fields: [serviceTicketBoardId], references: [id])
|
||||
ticketOwner User? @relation("ServiceTicketOwner", fields: [ticketOwnerId], references: [cwIdentifier])
|
||||
impact ServiceTicketImpact @relation(fields: [impactId], references: [id])
|
||||
priority ServiceTicketPriority @relation(fields: [priorityId], references: [id])
|
||||
source ServiceTicketSource @relation(fields: [sourceId], references: [id])
|
||||
location ServiceTicketLocation @relation(fields: [locationId], references: [id])
|
||||
serviceTicketBoard ServiceTicketBoard? @relation(fields: [serviceTicketBoardId], references: [id])
|
||||
ticketOwner User? @relation("ServiceTicketOwner", fields: [ticketOwnerId], references: [cwIdentifier])
|
||||
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
contact Contact? @relation(fields: [contactId], references: [id])
|
||||
@@ -1032,8 +1040,8 @@ model Opportunity {
|
||||
|
||||
generatedQuotes GeneratedQuotes[]
|
||||
|
||||
typeId Int
|
||||
type OpportunityType @relation(fields: [typeId], references: [id])
|
||||
typeId Int?
|
||||
type OpportunityType? @relation(fields: [typeId], references: [id])
|
||||
|
||||
stageId Int?
|
||||
stage OpportunityStage? @relation(fields: [stageId], references: [id])
|
||||
@@ -1104,9 +1112,9 @@ model ScheduleStatus {
|
||||
|
||||
name String
|
||||
description String? // Optima Only field, not synced to CW
|
||||
color String?
|
||||
color String?
|
||||
|
||||
softFlag Boolean @default(false)
|
||||
softFlag Boolean @default(false)
|
||||
defaultFlag Boolean @default(false)
|
||||
|
||||
schedules Schedule[]
|
||||
@@ -1122,15 +1130,15 @@ model ScheduleType {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
name String
|
||||
description String? // Optima Only field, not synced to CW
|
||||
displayColor String?
|
||||
name String
|
||||
description String? // Optima Only field, not synced to CW
|
||||
displayColor String?
|
||||
|
||||
tableReference String?
|
||||
moduleId String? @db.Char(2)
|
||||
moduleId String? @db.Char(2)
|
||||
scheduleTypeId String? @db.Char(1)
|
||||
|
||||
systemFlag Boolean @default(false)
|
||||
systemFlag Boolean @default(false)
|
||||
displayFlag Boolean @default(false)
|
||||
|
||||
schedules Schedule[]
|
||||
@@ -1143,15 +1151,15 @@ model ScheduleType {
|
||||
}
|
||||
|
||||
model ScheduleSpan {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
scheduleSpanId String? @db.Char(1)
|
||||
spanDesc String? @db.Char(20)
|
||||
spanDesc String? @db.Char(20)
|
||||
|
||||
schedules Schedule[]
|
||||
}
|
||||
|
||||
model Schedule {
|
||||
id Int @unique
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
name String
|
||||
@@ -1159,24 +1167,24 @@ model Schedule {
|
||||
|
||||
memberId String?
|
||||
|
||||
closedFlag Boolean @default(false)
|
||||
reminderFlag Boolean @default(false)
|
||||
allDayFlag Boolean @default(false)
|
||||
closedFlag Boolean @default(false)
|
||||
reminderFlag Boolean @default(false)
|
||||
allDayFlag Boolean @default(false)
|
||||
acknowledgementFlag Boolean @default(false)
|
||||
meetingFlag Boolean @default(false)
|
||||
recurringFlag Boolean @default(false)
|
||||
meetingFlag Boolean @default(false)
|
||||
recurringFlag Boolean @default(false)
|
||||
|
||||
billableFlag Boolean @default(false)
|
||||
|
||||
acknowledgedById String?
|
||||
acknowledgedAt DateTime?
|
||||
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
hoursScheduled Float?
|
||||
duration Int? // The number of days in between the start and end date.
|
||||
hoursPerDay Float?
|
||||
reminderMinutes Int? @default(15)
|
||||
acknowledgedById String?
|
||||
acknowledgedAt DateTime?
|
||||
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
hoursScheduled Float?
|
||||
duration Int? // The number of days in between the start and end date.
|
||||
hoursPerDay Float?
|
||||
reminderMinutes Int? @default(15)
|
||||
|
||||
statusId Int?
|
||||
status ScheduleStatus? @relation(fields: [statusId], references: [id])
|
||||
@@ -1189,9 +1197,274 @@ model Schedule {
|
||||
|
||||
updatedById String?
|
||||
createdById String?
|
||||
closedById String?
|
||||
closedById String?
|
||||
|
||||
closedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ------- Activities -------
|
||||
|
||||
model Activity {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
subject String // Activity Title/Summary
|
||||
|
||||
notes ActivityNotes?
|
||||
|
||||
startTime DateTime //Full Date Time Value
|
||||
endTime DateTime //Full Date Time Value
|
||||
|
||||
assignToId String
|
||||
assignedById String
|
||||
enteredBy String
|
||||
|
||||
automated Boolean @default(false)
|
||||
closedFlag Boolean @default(false)
|
||||
|
||||
notifyCompleteFlag Boolean @default(false) // Should we send a notification to the person assigned when activity is completed.
|
||||
notificationSentFlat Boolean @default(false) // Tracks to see if the completion notification has already been sent out.
|
||||
|
||||
opportunityId String?
|
||||
serviceTicketId String?
|
||||
|
||||
contactId Int?
|
||||
companyId Int?
|
||||
activityTypeId Int?
|
||||
activityStatusId Int?
|
||||
|
||||
contact Contact? @relation(fields: [contactId], references: [id])
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
activityType ActivityType? @relation(fields: [activityTypeId], references: [id])
|
||||
activityStatus ActivityStatus? @relation(fields: [activityStatusId], references: [id])
|
||||
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
createdById String?
|
||||
updatedById String?
|
||||
closedById String?
|
||||
|
||||
closedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ActivityNotes {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
notes String @default("")
|
||||
|
||||
activityId Int? @unique
|
||||
activity Activity? @relation(fields: [activityId], references: [id])
|
||||
|
||||
internalAnalysisFlag Boolean @default(false) // Does this note describing the internal analysis of the activity, such as root cause analysis or technical details that may not be relevant to the customer?
|
||||
|
||||
enteredById String?
|
||||
updatedById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ActivityType {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
name String @db.VarChar(15) // "SO_Activity_Type_ID" in CW
|
||||
description String
|
||||
|
||||
inactiveFlag Boolean @default(false)
|
||||
historyFlag Boolean @default(false) // Is this activity type just for historical record keeping, and should not be used for new activities?
|
||||
|
||||
defaultFlag Boolean @default(false)
|
||||
importFlag Boolean @default(false)
|
||||
emailFlag Boolean @default(false)
|
||||
memoFlag Boolean @default(false)
|
||||
|
||||
pointsValue Int?
|
||||
|
||||
activities Activity[]
|
||||
|
||||
updatedById String?
|
||||
createdById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ActivityStatus {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
name String
|
||||
description String? // Optima Only field, not synced to CW
|
||||
|
||||
closedFlag Boolean @default(false)
|
||||
inactiveFlag Boolean @default(false)
|
||||
defaultFlag Boolean @default(false)
|
||||
spawnFollowupFlag Boolean @default(false) // Should creating an activity with this status automatically spawn a follow-up activity with a different type and/or status?
|
||||
|
||||
activities Activity[]
|
||||
|
||||
updatedById String?
|
||||
createdById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ------- Time Entries -------
|
||||
|
||||
model TimeEntry {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
// The CW member identifier of the person who logged this time
|
||||
memberId String?
|
||||
|
||||
// Relational Values
|
||||
serviceTicketId Int?
|
||||
activityId Int?
|
||||
projectId Int? // TODO: Implement this.
|
||||
chargeCodeId Int?
|
||||
companyId Int
|
||||
statusId Int?
|
||||
locationId Int?
|
||||
contactId Int?
|
||||
|
||||
contact Contact? @relation(fields: [contactId], references: [id])
|
||||
location CorporateLocation? @relation(fields: [locationId], references: [id])
|
||||
status TimeEntryStatus? @relation(fields: [statusId], references: [id])
|
||||
chargeCode TimeEntryChargeCode? @relation(fields: [chargeCodeId], references: [id])
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
serviceTicket ServiceTicket? @relation(fields: [serviceTicketId], references: [id])
|
||||
activity Activity? @relation(fields: [activityId], references: [id])
|
||||
|
||||
// ------ Time Fields ------
|
||||
|
||||
dateStart DateTime?
|
||||
timeStart DateTime?
|
||||
timeEnd DateTime?
|
||||
|
||||
// ------ Notes ------
|
||||
|
||||
notes String? // Customer-visible notes about what was done
|
||||
notesMd String? // Markdown version of notes
|
||||
internalNote String? // Internal notes not visible to the customer
|
||||
|
||||
// ------ Hours ------
|
||||
|
||||
billableHours Float? // How many hours are being billed to the customer
|
||||
actualHours Float? // How many hours were actually worked
|
||||
invoicedHours Float? // How many hours have been included on an invoice
|
||||
deductedHours Float? // How many hours were deducted from billing, but not necessarily the same as actual hours if there are any adjustments or overrides.
|
||||
|
||||
// ------ Rates ------
|
||||
|
||||
hourlyRate Float? // The rate at which this time is billed to the customer
|
||||
effectiveRate Float? // The actual effective rate after any agreement or override adjustments
|
||||
|
||||
// ------ Flag Fields ------
|
||||
|
||||
issueFlag Boolean @default(false)
|
||||
mergedFlag Boolean @default(false) // Has this time entry been merged with another time entry, such as when multiple time entries are combined into one for billing purposes?
|
||||
invoiceFlag Boolean @default(false) // Has this time entry been included on an invoice?
|
||||
billableFlag Boolean @default(true) // Should this time entry be billed to the customer?
|
||||
documentFlag Boolean @default(false) // Is there a document associated with this time entry, such as a receipt for an expense or a report of the work done?
|
||||
teProblemFlag Boolean @default(false) // Does this note describe the problem?
|
||||
teResolutionFlag Boolean @default(false) // Does this note describe the resolution?
|
||||
teInternalAnalysisFlag Boolean @default(false) // Does this note contain internal analysis?
|
||||
|
||||
// ------ Audit Fields ------
|
||||
|
||||
chargeToRecId Int? // The record ID of the entity that this time entry should be charged to, which may be used for billing and reporting purposes, and may be different from the service ticket or activity it is associated with.
|
||||
chargeToType String? @db.VarChar(13)
|
||||
|
||||
createdById String?
|
||||
updatedById String?
|
||||
originalAuthorId String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TimeEntryStatus {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
statusId Int @unique
|
||||
description String? @db.VarChar(50)
|
||||
action String?
|
||||
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TimeEntryChargeCode {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
chargeCodeId Int @unique
|
||||
description String? @db.VarChar(50)
|
||||
|
||||
expenseFlag Boolean @default(false)
|
||||
timeFlag Boolean @default(true)
|
||||
billableFlag Boolean @default(true)
|
||||
invoiceFlag Boolean @default(true)
|
||||
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
updatedById String?
|
||||
createdById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TimeActivityClass {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
description String? @db.VarChar(50)
|
||||
hourlyRate Float?
|
||||
|
||||
inactiveFlag Boolean @default(false)
|
||||
taxExemptFlag Boolean @default(false)
|
||||
|
||||
createdById String?
|
||||
updatedById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TimeActivityType {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
description String? @db.VarChar(50)
|
||||
|
||||
minHours Float?
|
||||
maxHours Float?
|
||||
|
||||
rate Float? @default(1)
|
||||
costMultiplier Float @default(1)
|
||||
|
||||
inactiveFlag Boolean @default(false)
|
||||
invoiceFlag Boolean @default(false)
|
||||
billableFlag Boolean @default(false)
|
||||
utilizationFlag Boolean @default(false)
|
||||
defaultFlag Boolean @default(false)
|
||||
multiplierFlag Boolean @default(false)
|
||||
|
||||
createdById String?
|
||||
updatedById String?
|
||||
|
||||
closedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -1269,12 +1542,12 @@ model GeneratedQuotes {
|
||||
}
|
||||
|
||||
model TaxCode {
|
||||
id Int @unique
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
opportunities Opportunity[]
|
||||
|
||||
code String @unique
|
||||
code String @unique
|
||||
codeCaption String
|
||||
description String?
|
||||
|
||||
@@ -1307,3 +1580,18 @@ model CwMember {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CwMemberType {
|
||||
id Int @unique
|
||||
uid String @id @default(uuid())
|
||||
|
||||
description String? @db.VarChar(30)
|
||||
|
||||
inactiveFlag Boolean @default(false)
|
||||
|
||||
updatedById String?
|
||||
createdById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
tableName | syncMode | recordsProcessed | recordsInserted | recordsSkipped | recordsFailed | createdAt
|
||||
--------------+-------------+------------------+-----------------+----------------+---------------+-------------------------
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:57.769
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:33.239
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:04.175
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:09:40.038
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:09:15.606
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:08:50.332
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:08:22.615
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:56.832
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:31.662
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:06.055
|
||||
(10 rows)
|
||||
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
|
||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
||||
|
||||
Commands marked with * may be preceded by a number, _N.
|
||||
Notes in parentheses indicate the behavior if _N is given.
|
||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
||||
|
||||
h H Display this help.
|
||||
q :q Q :Q ZZ Exit.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMOOVVIINNGG
|
||||
|
||||
e ^E j ^N CR * Forward one line (or _N lines).
|
||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
||||
b ^B ESC-v * Backward one window (or _N lines).
|
||||
z * Forward one window (and set window to _N).
|
||||
w * Backward one window (and set window to _N).
|
||||
ESC-SPACE * Forward one window, but don't stop at end-of-file.
|
||||
d ^D * Forward one half-window (and set half-window to _N).
|
||||
u ^U * Backward one half-window (and set half-window to _N).
|
||||
ESC-) RightArrow * Right one half screen width (or _N positions).
|
||||
ESC-( LeftArrow * Left one half screen width (or _N positions).
|
||||
ESC-} ^RightArrow Right to last column displayed.
|
||||
ESC-{ ^LeftArrow Left to first column.
|
||||
F Forward forever; like "tail -f".
|
||||
ESC-F Like F but stop when search pattern is found.
|
||||
r ^R ^L Repaint screen.
|
||||
R Repaint screen, discarding buffered input.
|
||||
---------------------------------------------------
|
||||
Default "window" is the screen height.
|
||||
Default "half-window" is half of the screen height.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
SSEEAARRCCHHIINNGG
|
||||
|
||||
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
|
||||
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
|
||||
n * Repeat previous search (for _N-th occurrence).
|
||||
N * Repeat previous search in reverse direction.
|
||||
ESC-n * Repeat previous search, spanning files.
|
||||
ESC-N * Repeat previous search, reverse dir. & spanning files.
|
||||
ESC-u Undo (toggle) search highlighting.
|
||||
ESC-U Clear search highlighting.
|
||||
&_p_a_t_t_e_r_n * Display only matching lines.
|
||||
---------------------------------------------------
|
||||
A search pattern may begin with one or more of:
|
||||
^N or ! Search for NON-matching lines.
|
||||
^E or * Search multiple files (pass thru END OF FILE).
|
||||
^F or @ Start search at FIRST file (for /) or last file (for ?).
|
||||
^K Highlight matches, but don't move (KEEP position).
|
||||
^R Don't use REGULAR EXPRESSIONS.
|
||||
^W WRAP search if no match found.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
JJUUMMPPIINNGG
|
||||
|
||||
g < ESC-< * Go to first line in file (or line _N).
|
||||
G > ESC-> * Go to last line in file (or line _N).
|
||||
p % * Go to beginning of file (or _N percent into file).
|
||||
t * Go to the (_N-th) next tag.
|
||||
T * Go to the (_N-th) previous tag.
|
||||
{ ( [ * Find close bracket } ) ].
|
||||
} ) ] * Find open bracket { ( [.
|
||||
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
|
||||
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
|
||||
---------------------------------------------------
|
||||
Each "find close bracket" command goes forward to the close bracket
|
||||
matching the (_N-th) open bracket in the top line.
|
||||
Each "find open bracket" command goes backward to the open bracket
|
||||
matching the (_N-th) close bracket in the bottom line.
|
||||
|
||||
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
|
||||
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
|
||||
'_<_l_e_t_t_e_r_> Go to a previously marked position.
|
||||
'' Go to the previous position.
|
||||
^X^X Same as '.
|
||||
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
|
||||
---------------------------------------------------
|
||||
A mark is any upper-case or lower-case letter.
|
||||
Certain marks are predefined:
|
||||
^ means beginning of the file
|
||||
$ means end of the file
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CCHHAANNGGIINNGG FFIILLEESS
|
||||
|
||||
:e [_f_i_l_e] Examine a new file.
|
||||
^X^V Same as :e.
|
||||
:n * Examine the (_N-th) next file from the command line.
|
||||
:p * Examine the (_N-th) previous file from the command line.
|
||||
:x * Examine the first (or _N-th) file from the command line.
|
||||
:d Delete the current file from the command line list.
|
||||
= ^G :f Print current file name.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
|
||||
|
||||
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
|
||||
--_<_n_a_m_e_> Toggle a command line option, by name.
|
||||
__<_f_l_a_g_> Display the setting of a command line option.
|
||||
___<_n_a_m_e_> Display the setting of an option, by name.
|
||||
+_c_m_d Execute the less cmd each time a new file is examined.
|
||||
|
||||
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|
||||
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
|
||||
s _f_i_l_e Save input to a file.
|
||||
v Edit the current file with $VISUAL or $EDITOR.
|
||||
V Print version number of "less".
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
OOPPTTIIOONNSS
|
||||
|
||||
Most options may be changed either on the command line,
|
||||
or from within less by using the - or -- command.
|
||||
Options may be given in one of two forms: either a single
|
||||
character preceded by a -, or a name preceded by --.
|
||||
|
||||
-? ........ --help
|
||||
Display help (from command line).
|
||||
-a ........ --search-skip-screen
|
||||
Search skips current screen.
|
||||
-A ........ --SEARCH-SKIP-SCREEN
|
||||
Search starts just after target line.
|
||||
-b [_N] .... --buffers=[_N]
|
||||
Number of buffers.
|
||||
-B ........ --auto-buffers
|
||||
Don't automatically allocate buffers for pipes.
|
||||
-c ........ --clear-screen
|
||||
Repaint by clearing rather than scrolling.
|
||||
-d ........ --dumb
|
||||
Dumb terminal.
|
||||
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
|
||||
Set screen colors.
|
||||
-e -E .... --quit-at-eof --QUIT-AT-EOF
|
||||
Quit at end of file.
|
||||
-f ........ --force
|
||||
Force open non-regular files.
|
||||
-F ........ --quit-if-one-screen
|
||||
Quit if entire file fits on first screen.
|
||||
-g ........ --hilite-search
|
||||
Highlight only last match for searches.
|
||||
-G ........ --HILITE-SEARCH
|
||||
Don't highlight any matches for searches.
|
||||
-h [_N] .... --max-back-scroll=[_N]
|
||||
Backward scroll limit.
|
||||
-i ........ --ignore-case
|
||||
Ignore case in searches that do not contain uppercase.
|
||||
-I ........ --IGNORE-CASE
|
||||
Ignore case in all searches.
|
||||
-j [_N] .... --jump-target=[_N]
|
||||
Screen position of target lines.
|
||||
-J ........ --status-column
|
||||
Display a status column at left edge of screen.
|
||||
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
|
||||
Use a lesskey file.
|
||||
-K ........ --quit-on-intr
|
||||
Exit less in response to ctrl-C.
|
||||
-L ........ --no-lessopen
|
||||
Ignore the LESSOPEN environment variable.
|
||||
-m -M .... --long-prompt --LONG-PROMPT
|
||||
Set prompt style.
|
||||
-n -N .... --line-numbers --LINE-NUMBERS
|
||||
Don't use line numbers.
|
||||
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
|
||||
Copy to log file (standard input only).
|
||||
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
|
||||
Copy to log file (unconditionally overwrite).
|
||||
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
|
||||
Start at pattern (from command line).
|
||||
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
|
||||
Define new prompt.
|
||||
-q -Q .... --quiet --QUIET --silent --SILENT
|
||||
Quiet the terminal bell.
|
||||
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
|
||||
Output "raw" control characters.
|
||||
-s ........ --squeeze-blank-lines
|
||||
Squeeze multiple blank lines.
|
||||
-S ........ --chop-long-lines
|
||||
Chop (truncate) long lines rather than wrapping.
|
||||
-t [_t_a_g] .. --tag=[_t_a_g]
|
||||
Find a tag.
|
||||
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
|
||||
Use an alternate tags file.
|
||||
-u -U .... --underline-special --UNDERLINE-SPECIAL
|
||||
Change handling of backspaces.
|
||||
-V ........ --version
|
||||
Display the version number of "less".
|
||||
-w ........ --hilite-unread
|
||||
Highlight first new line after forward-screen.
|
||||
-W ........ --HILITE-UNREAD
|
||||
Highlight first new line after any forward movement.
|
||||
-x [_N[,...]] --tabs=[_N[,...]]
|
||||
Set tab stops.
|
||||
-X ........ --no-init
|
||||
Don't use termcap init/deinit strings.
|
||||
-y [_N] .... --max-forw-scroll=[_N]
|
||||
Forward scroll limit.
|
||||
-z [_N] .... --window=[_N]
|
||||
Set size of window.
|
||||
-" [_c[_c]] . --quotes=[_c[_c]]
|
||||
Set shell quote characters.
|
||||
-~ ........ --tilde
|
||||
Don't display tildes after end of file.
|
||||
-# [_N] .... --shift=[_N]
|
||||
Set horizontal scroll amount (0 = one half screen width).
|
||||
--file-size
|
||||
Automatically determine the size of the input file.
|
||||
--follow-name
|
||||
The F command changes files if the input file is renamed.
|
||||
--incsearch
|
||||
Search file as each pattern character is typed in.
|
||||
--line-num-width=N
|
||||
Set the width of the -N line number field to N characters.
|
||||
--mouse
|
||||
Enable mouse input.
|
||||
--no-keypad
|
||||
Don't send termcap keypad init/deinit strings.
|
||||
--no-histdups
|
||||
Remove duplicates from command history.
|
||||
--rscroll=C
|
||||
Set the character used to mark truncated lines.
|
||||
--save-marks
|
||||
Retain marks across invocations of less.
|
||||
--status-col-width=N
|
||||
Set the width of the -J status column to N characters.
|
||||
--use-backslash
|
||||
Subsequent options use backslash as escape char.
|
||||
--use-color
|
||||
Enables colored text.
|
||||
--wheel-lines=N
|
||||
Each click of the mouse wheel moves N lines.
|
||||
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
LLIINNEE EEDDIITTIINNGG
|
||||
|
||||
These keys can be used to edit text being entered
|
||||
on the "command line" at the bottom of the screen.
|
||||
|
||||
RightArrow ..................... ESC-l ... Move cursor right one character.
|
||||
LeftArrow ...................... ESC-h ... Move cursor left one character.
|
||||
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
|
||||
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
|
||||
HOME ........................... ESC-0 ... Move cursor to start of line.
|
||||
END ............................ ESC-$ ... Move cursor to end of line.
|
||||
BACKSPACE ................................ Delete char to left of cursor.
|
||||
DELETE ......................... ESC-x ... Delete char under cursor.
|
||||
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
|
||||
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
|
||||
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
|
||||
UpArrow ........................ ESC-k ... Retrieve previous command line.
|
||||
DownArrow ...................... ESC-j ... Retrieve next command line.
|
||||
TAB ...................................... Complete filename & cycle.
|
||||
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
|
||||
ctrl-L ................................... Complete filename, list all.
|
||||
@@ -0,0 +1,14 @@
|
||||
tableName | syncMode | recordsProcessed | recordsInserted | recordsSkipped | recordsFailed | createdAt
|
||||
--------------+-------------+------------------+-----------------+----------------+---------------+-------------------------
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:57.769
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:33.239
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:04.175
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:09:40.038
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:09:15.606
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:08:50.332
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:08:22.615
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:56.832
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:31.662
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:06.055
|
||||
(10 rows)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { companies } from "../../../managers/companies";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/company/companies/[id] */
|
||||
@@ -12,23 +13,42 @@ export default createRoute(
|
||||
["/companies/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const company = await companies.fetch(c.req.param("identifier"));
|
||||
const user = c.get("user");
|
||||
const includeAddress =
|
||||
c.req.query("includeAddress") === "true" &&
|
||||
!!user &&
|
||||
(await user.hasPermission("company.fetch.address"));
|
||||
const company = await companies.fetch(c.req.param("identifier") as string);
|
||||
const includeAddress = c.req.query("includeAddress") === "true";
|
||||
const includePrimaryContact =
|
||||
c.req.query("includePrimaryContact") === "true";
|
||||
const includeAllContacts =
|
||||
c.req.query("includeAllContacts") === "true" &&
|
||||
!!user &&
|
||||
(await user.hasPermission("company.fetch.contacts"));
|
||||
const includeAllContacts = c.req.query("includeAllContacts") === "true";
|
||||
const includeAllAddresses = c.req.query("includeAllAddresses") === "true";
|
||||
|
||||
// Check for address-specific permission if includeAddress is requested
|
||||
if (includeAddress) {
|
||||
const user = c.get("user");
|
||||
if (!user || !(await user.hasPermission("company.fetch.address"))) {
|
||||
throw new GenericError({
|
||||
name: "InsufficientPermission",
|
||||
message: "You do not have permission to view company addresses.",
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for contacts permission if includeAllContacts is requested
|
||||
if (includeAllContacts) {
|
||||
const user = c.get("user");
|
||||
if (!user || !(await user.hasPermission("company.fetch.contacts"))) {
|
||||
throw new GenericError({
|
||||
name: "InsufficientPermission",
|
||||
message: "You do not have permission to view company contacts.",
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const companyData = company.toJson({
|
||||
includeAddress,
|
||||
includePrimaryContact,
|
||||
includeAllContacts,
|
||||
includeAllAddresses,
|
||||
});
|
||||
const gatedData = await processObjectValuePerms(
|
||||
companyData,
|
||||
@@ -36,13 +56,6 @@ export default createRoute(
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
// cw_Data fields were already gated by the explicit permission checks above
|
||||
// (company.fetch.contacts / company.fetch.address). Re-attach them so they
|
||||
// are not silently dropped by field-level gating on obj.company.cw_Data.
|
||||
if (companyData.cw_Data && Object.keys(companyData.cw_Data).length > 0) {
|
||||
(gatedData as any).cw_Data = companyData.cw_Data;
|
||||
}
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company Fetched Successfully!",
|
||||
gatedData,
|
||||
|
||||
@@ -35,8 +35,6 @@ export default createRoute(
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
console.log("Creating Credential Type with data:", data);
|
||||
|
||||
const credentialType = await credentialTypes.create(data as any);
|
||||
|
||||
const response = apiResponse.created(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { getBoss } from "../../workert";
|
||||
import { getBoss } from "../../boss-instance";
|
||||
import { WorkerQueue } from "../../modules/workers/queues";
|
||||
|
||||
/* POST /v1/cw/sync/full */
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { prisma } from "../../../constants";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/procurement/items/:identifier/inventory */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items/:identifier/inventory"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const includeWarehouse = c.req.query("includeWarehouse") === "true";
|
||||
const includeWarehouseBin = c.req.query("includeWarehouseBin") === "true";
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
const rows = await prisma.productInventory.findMany({
|
||||
where: { itemId: item.cwCatalogId },
|
||||
include: {
|
||||
warehouse: includeWarehouse,
|
||||
warehouseBin: includeWarehouseBin,
|
||||
},
|
||||
orderBy: [{ warehouseId: "asc" }, { warehouseBinId: "asc" }],
|
||||
});
|
||||
|
||||
const data = rows.map((row) => ({
|
||||
id: row.id,
|
||||
qtyOnHand: row.qtyOnHand,
|
||||
warehouseId: row.warehouseId,
|
||||
warehouseBinId: row.warehouseBinId,
|
||||
...(includeWarehouse && {
|
||||
warehouse: (row as any).warehouse
|
||||
? { id: (row as any).warehouse.id, name: (row as any).warehouse.name }
|
||||
: null,
|
||||
}),
|
||||
...(includeWarehouseBin && {
|
||||
warehouseBin: (row as any).warehouseBin
|
||||
? { id: (row as any).warehouseBin.id, name: (row as any).warehouseBin.name }
|
||||
: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Product inventory fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as fetch } from "./[id]/fetch";
|
||||
import { default as refreshInventory } from "./[id]/refreshInventory";
|
||||
import { default as inventoryByWarehouse } from "./[id]/inventoryByWarehouse";
|
||||
import { default as link } from "./[id]/link";
|
||||
import { default as unlink } from "./[id]/unlink";
|
||||
import { default as fetchLinked } from "./[id]/fetchLinked";
|
||||
@@ -15,6 +16,7 @@ export {
|
||||
fetchAll,
|
||||
fetchLinked,
|
||||
filters,
|
||||
inventoryByWarehouse,
|
||||
link,
|
||||
refreshInventory,
|
||||
unlink,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as timeEntryRoutes from "../time-entries";
|
||||
|
||||
const timeEntryRouter = new Hono();
|
||||
Object.values(timeEntryRoutes).map((r) => timeEntryRouter.route("/", r));
|
||||
|
||||
export default timeEntryRouter;
|
||||
@@ -36,7 +36,10 @@ export default createRoute(
|
||||
if (includes.has("products")) {
|
||||
subResourcePromises.products = item
|
||||
.fetchProducts()
|
||||
.then((products) => products.map((p) => p.toJson()));
|
||||
.then((products) => {
|
||||
const json = products.map((p) => p.toJson());
|
||||
return json;
|
||||
});
|
||||
}
|
||||
if (includes.has("quotes")) {
|
||||
subResourcePromises.quotes = generatedQuotes
|
||||
|
||||
@@ -34,6 +34,8 @@ import { default as fetchByUserId } from "./opportunities/fetchByUserId";
|
||||
import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
|
||||
import { default as workflowStatus } from "./opportunities/[id]/workflow/status";
|
||||
import { default as workflowHistory } from "./opportunities/[id]/workflow/history";
|
||||
import { default as addTime } from "./opportunities/[id]/time";
|
||||
import { default as fetchActivities } from "./opportunities/[id]/activities";
|
||||
|
||||
export {
|
||||
addProduct,
|
||||
@@ -72,4 +74,6 @@ export {
|
||||
workflowDispatch,
|
||||
workflowStatus,
|
||||
workflowHistory,
|
||||
addTime,
|
||||
fetchActivities,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
import { opportunities } from "../../../../managers/opportunities";
|
||||
import { activityCw } from "../../../../modules/cw-utils/activities/activities";
|
||||
import { OptimaType } from "../../../../workflows/wf.opportunity";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier/activities */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/opportunity/:identifier/activities"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
|
||||
const opportunity = await opportunities.fetchItem(identifier);
|
||||
|
||||
const rawActivities = await activityCw.fetchByOpportunityDirect(
|
||||
opportunity.cwOpportunityId,
|
||||
);
|
||||
|
||||
// Return only open workflow activities (status != 2)
|
||||
const openActivities = rawActivities
|
||||
.filter((a: any) => a.status?.id !== 2)
|
||||
.map((a: any) => {
|
||||
const optimaTypeField = a.customFields?.find(
|
||||
(f: any) => f.id === OptimaType.FIELD_ID || f.caption === "Optima_Type",
|
||||
);
|
||||
const parentActivityField = a.customFields?.find(
|
||||
(f: any) => f.id === 50 || f.caption === "Parent_Activity",
|
||||
);
|
||||
return {
|
||||
cwActivityId: a.id,
|
||||
name: a.name,
|
||||
optimaType: optimaTypeField?.value ?? null,
|
||||
parentActivityId: parentActivityField?.value
|
||||
? parseInt(parentActivityField.value, 10) || null
|
||||
: null,
|
||||
status: a.status,
|
||||
dateStart: a.dateStart ?? null,
|
||||
dateEnd: a.dateEnd ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
const response = apiResponse.successful("Open activities fetched.", {
|
||||
activities: openActivities,
|
||||
});
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.view"] }),
|
||||
);
|
||||
@@ -39,7 +39,11 @@ export default createRoute(
|
||||
if (includes.has("products")) {
|
||||
subResourcePromises.products = item
|
||||
.fetchProducts()
|
||||
.then((products) => products.map((p) => p.toJson()));
|
||||
.then((products) => {
|
||||
const json = products.map((p) => p.toJson());
|
||||
console.log(`[PRODUCTS_DEBUG] cwOpportunityId=${item.cwOpportunityId} count=${json.length}`, JSON.stringify(json, null, 2));
|
||||
return json;
|
||||
});
|
||||
}
|
||||
if (includes.has("quotes")) {
|
||||
const includeRegenData = c.req.query("includeRegenData") === "true";
|
||||
|
||||
@@ -7,14 +7,23 @@ import { z } from "zod";
|
||||
import { cwMembers } from "../../../../../managers/cwMembers";
|
||||
import {
|
||||
createWorkflowActivity,
|
||||
resolveQuoteParentActivityCwId,
|
||||
OptimaType,
|
||||
OpportunityStatus,
|
||||
} from "../../../../../workflows/wf.opportunity";
|
||||
|
||||
/** Status IDs that do NOT require the backgenerate permission. */
|
||||
const STANDARD_GENERATE_STATUSES = new Set<number>([
|
||||
OpportunityStatus.New,
|
||||
OpportunityStatus.Active,
|
||||
]);
|
||||
|
||||
const commitQuoteSchema = z
|
||||
.object({
|
||||
lineItemPricing: z.boolean().optional(),
|
||||
includeQuoteNarrative: z.boolean().optional(),
|
||||
includeItemNarratives: z.boolean().optional(),
|
||||
separateRecurringServices: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
@@ -32,6 +41,28 @@ export default createRoute(
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
const user = c.get("user");
|
||||
|
||||
// If the opportunity is in the Optima workflow and NOT in a standard generate state
|
||||
// (New or Active), require the backgenerate permission.
|
||||
if (
|
||||
item.stageName === "Optima" &&
|
||||
item.statusCwId != null &&
|
||||
!STANDARD_GENERATE_STATUSES.has(item.statusCwId)
|
||||
) {
|
||||
const canBackGenerate = await user.hasPermission(
|
||||
"sales.opportunity.quote.commit.backgenerate",
|
||||
);
|
||||
if (!canBackGenerate) {
|
||||
return c.json(
|
||||
{
|
||||
successful: false,
|
||||
message:
|
||||
"Generating a quote in this workflow state requires the 'sales.opportunity.quote.commit.backgenerate' permission.",
|
||||
},
|
||||
403,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const quote = await item.commitQuote(opts ?? {}, user);
|
||||
|
||||
// Create a workflow activity for the generated quote
|
||||
@@ -44,6 +75,10 @@ export default createRoute(
|
||||
}
|
||||
|
||||
if (cwMemberId) {
|
||||
const parentActivityCwId = await resolveQuoteParentActivityCwId(
|
||||
item.cwOpportunityId,
|
||||
);
|
||||
|
||||
await createWorkflowActivity({
|
||||
name: `[Workflow] Quote generated — ${item.name}`,
|
||||
opportunityCwId: item.cwOpportunityId,
|
||||
@@ -52,6 +87,7 @@ export default createRoute(
|
||||
notes: `Quote "${quote.quoteFileName}" generated.`,
|
||||
optimaType: OptimaType.QuoteGenerated,
|
||||
quoteId: quote.id,
|
||||
parentActivityCwId,
|
||||
});
|
||||
}
|
||||
} catch (activityErr) {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { z } from "zod";
|
||||
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
import { opportunities } from "../../../../managers/opportunities";
|
||||
import { cwMembers } from "../../../../managers/cwMembers";
|
||||
import { activityCw } from "../../../../modules/cw-utils/activities/activities";
|
||||
import { submitTimeEntry } from "../../../../services/cw.opportunityService";
|
||||
import { OptimaType } from "../../../../workflows/wf.opportunity";
|
||||
import GenericError from "../../../../Errors/GenericError";
|
||||
|
||||
const addTimeSchema = z.object({
|
||||
/** CW activity ID to log time against. */
|
||||
activityId: z.number().int().positive(),
|
||||
/** ISO-8601 datetime when work started. */
|
||||
timeStarted: z.string().datetime(),
|
||||
/** ISO-8601 datetime when work ended. */
|
||||
timeEnded: z.string().datetime(),
|
||||
/** Optional notes for the time entry. */
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
/* POST /v1/sales/opportunities/opportunity/:identifier/time */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/opportunity/:identifier/time"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
const data = addTimeSchema.parse(body);
|
||||
|
||||
const user = c.get("user");
|
||||
|
||||
if (!user.cwIdentifier) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "MissingCwIdentifier",
|
||||
message:
|
||||
"Your account is not linked to a ConnectWise member. A CW member association is required to log time.",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the opportunity exists and belongs to the correct identifier
|
||||
const opportunity = await opportunities.fetchItem(identifier);
|
||||
|
||||
const cwMember = await cwMembers.fetch(user.cwIdentifier);
|
||||
|
||||
// Fetch the activity to verify it belongs to this opportunity and is open
|
||||
const activity = await activityCw.fetch(data.activityId);
|
||||
|
||||
if (activity.opportunity?.id !== opportunity.cwOpportunityId) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "ActivityMismatch",
|
||||
message: "The specified activity does not belong to this opportunity.",
|
||||
});
|
||||
}
|
||||
|
||||
if (activity.status?.id === 2) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "ActivityClosed",
|
||||
message: "Cannot log time against a closed activity.",
|
||||
});
|
||||
}
|
||||
|
||||
// Submit the time entry
|
||||
const result = await submitTimeEntry({
|
||||
activityId: data.activityId,
|
||||
cwMemberId: cwMember.cwMemberId,
|
||||
timeStart: data.timeStarted,
|
||||
timeEnd: data.timeEnded,
|
||||
notes: data.notes ?? "",
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new GenericError({
|
||||
status: 502,
|
||||
name: "TimeEntryFailed",
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
|
||||
// If the activity is a Schedule Entry (Optima_Type = "Schedule Entry"),
|
||||
// close it now that time has been logged against it.
|
||||
const optimaTypeField = activity.customFields?.find(
|
||||
(f: any) => f.id === OptimaType.FIELD_ID || f.caption === "Optima_Type",
|
||||
);
|
||||
if (optimaTypeField?.value === OptimaType.ScheduleEntry) {
|
||||
try {
|
||||
await activityCw.update(data.activityId, [
|
||||
{ op: "replace", path: "status", value: { id: 2 } },
|
||||
]);
|
||||
} catch (closeErr) {
|
||||
// Non-fatal — time entry was already submitted successfully
|
||||
console.error(
|
||||
`[AddTime] Failed to close Schedule Entry activity ${data.activityId}:`,
|
||||
closeErr,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response = apiResponse.successful("Time entry submitted successfully.", {
|
||||
cwTimeEntryId: result.cwTimeEntryId,
|
||||
activityId: data.activityId,
|
||||
activityWasClosed: optimaTypeField?.value === OptimaType.ScheduleEntry,
|
||||
});
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.update"] }),
|
||||
);
|
||||
@@ -5,12 +5,18 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
import GenericError from "../../../../Errors/GenericError";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
OpportunityStatus,
|
||||
StatusIdToKey,
|
||||
} from "../../../../workflows/wf.opportunity";
|
||||
import { resolveCwProbabilityId } from "../../../../modules/cw-utils/opportunities/cwProbabilityCache";
|
||||
|
||||
const updateSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).optional(),
|
||||
notes: z.string().optional(),
|
||||
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
|
||||
probability: z.number().min(0).max(100).optional(),
|
||||
rating: z.object({ id: z.number() }).optional(),
|
||||
type: z.object({ id: z.number() }).optional(),
|
||||
stage: z.object({ id: z.number() }).optional(),
|
||||
@@ -46,8 +52,35 @@ export default createRoute(
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
|
||||
// Read-only guard: only New and Active statuses allow opportunity data mutations.
|
||||
const editableStatuses = new Set<number>([
|
||||
OpportunityStatus.New,
|
||||
OpportunityStatus.Active,
|
||||
]);
|
||||
const currentStatusId = item.statusCwId ?? null;
|
||||
if (currentStatusId !== null && !editableStatuses.has(currentStatusId)) {
|
||||
const statusKey = StatusIdToKey[currentStatusId] ?? `ID ${currentStatusId}`;
|
||||
throw new GenericError({
|
||||
status: 422,
|
||||
name: "OpportunityReadOnly",
|
||||
message: `Opportunity data cannot be edited in "${statusKey}" status. Only "New" and "Active" opportunities are editable.`,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await item.updateOpportunity(data);
|
||||
const { probability: probabilityPercent, ...rest } = data;
|
||||
|
||||
// Resolve numeric probability → CW reference ID
|
||||
let probabilityRef: { id: number } | undefined;
|
||||
if (probabilityPercent !== undefined) {
|
||||
const probId = await resolveCwProbabilityId(probabilityPercent);
|
||||
if (probId != null) probabilityRef = { id: probId };
|
||||
}
|
||||
|
||||
const updated = await item.updateOpportunity({
|
||||
...rest,
|
||||
...(probabilityRef !== undefined ? { probability: probabilityRef } : {}),
|
||||
});
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity updated successfully!",
|
||||
|
||||
@@ -91,6 +91,19 @@ const dispatchSchema = z.discriminatedUnion("action", [
|
||||
action: z.literal("reopen"),
|
||||
payload: noteRequiredPayload,
|
||||
}),
|
||||
z.object({
|
||||
action: z.literal("sendBackForRevision"),
|
||||
payload: noteRequiredPayload,
|
||||
}),
|
||||
z.object({
|
||||
action: z.literal("createScheduleEntry"),
|
||||
payload: basePayload.extend({
|
||||
activityTypeValue: z.enum(["Follow-Up", "Appointment", "Admin"]),
|
||||
dueDate: z.string().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
// ── Route ─────────────────────────────────────────────────────────────────
|
||||
@@ -103,15 +116,7 @@ export default createRoute(
|
||||
try {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
console.log(
|
||||
"[Workflow Dispatch] Raw request body:",
|
||||
JSON.stringify(body, null, 2),
|
||||
);
|
||||
const parsed = dispatchSchema.parse(body);
|
||||
console.log(
|
||||
"[Workflow Dispatch] Parsed payload:",
|
||||
JSON.stringify(parsed.payload, null, 2),
|
||||
);
|
||||
const user = c.get("user");
|
||||
|
||||
// ── Resolve opportunity ────────────────────────────────────────────
|
||||
|
||||
@@ -3,9 +3,11 @@ import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { timeEntries } from "../../../../../managers/timeEntries";
|
||||
import { activityCw } from "../../../../../modules/cw-utils/activities/activities";
|
||||
import { ActivityController } from "../../../../../controllers/ActivityController";
|
||||
import { OptimaType } from "../../../../../workflows/wf.opportunity";
|
||||
import { prisma } from "../../../../../constants";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// HELPERS
|
||||
@@ -22,6 +24,7 @@ const OPTIMA_TYPE_VALUES = new Set<string>([
|
||||
OptimaType.Revision,
|
||||
OptimaType.Finalized,
|
||||
OptimaType.Converted,
|
||||
OptimaType.ScheduleEntry,
|
||||
]);
|
||||
|
||||
/** QuoteID custom field ID (matches wf.opportunity.ts QUOTE_ID_FIELD_ID). */
|
||||
@@ -30,6 +33,9 @@ const QUOTE_ID_FIELD_ID = 48;
|
||||
/** Close Date custom field ID (matches wf.opportunity.ts CLOSE_DATE_FIELD_ID). */
|
||||
const CLOSE_DATE_FIELD_ID = 49;
|
||||
|
||||
/** Parent_Activity custom field ID. */
|
||||
const PARENT_ACTIVITY_FIELD_ID = 50;
|
||||
|
||||
/**
|
||||
* Extract the Optima_Type value from a CW activity's custom fields.
|
||||
* Returns the string value if present, or null.
|
||||
@@ -69,6 +75,22 @@ function extractCloseDate(
|
||||
return field.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the Parent_Activity custom field value from a CW activity.
|
||||
* Returns the numeric activity ID or null.
|
||||
*/
|
||||
function extractParentActivityId(
|
||||
customFields: { id: number; value: unknown }[] | undefined,
|
||||
): number | null {
|
||||
if (!customFields) return null;
|
||||
const field = customFields.find(
|
||||
(f) => f.id === PARENT_ACTIVITY_FIELD_ID || (f as any).caption === "Parent_Activity",
|
||||
);
|
||||
if (!field?.value) return null;
|
||||
const parsed = parseInt(String(field.value), 10);
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ROUTE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -95,6 +117,7 @@ export default createRoute(
|
||||
activity: ReturnType<ActivityController["toJson"]>;
|
||||
optimaType: string;
|
||||
quoteId: string | null;
|
||||
parentActivityId: number | null;
|
||||
closed: boolean;
|
||||
closedAt: string | null;
|
||||
}[] = [];
|
||||
@@ -108,6 +131,7 @@ export default createRoute(
|
||||
if (filterType && optimaType !== filterType) continue;
|
||||
|
||||
const quoteId = extractQuoteId(raw.customFields);
|
||||
const parentActivityId = extractParentActivityId(raw.customFields);
|
||||
const closed = raw.status?.id === 2;
|
||||
const closedAt = extractCloseDate(raw.customFields);
|
||||
|
||||
@@ -115,6 +139,7 @@ export default createRoute(
|
||||
activity: json,
|
||||
optimaType,
|
||||
quoteId,
|
||||
parentActivityId,
|
||||
closed,
|
||||
closedAt,
|
||||
});
|
||||
@@ -131,13 +156,60 @@ export default createRoute(
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
// Attach time entries for each activity in parallel
|
||||
const activitiesWithTimeEntries = await Promise.all(
|
||||
workflowActivities.map(async (item) => {
|
||||
const entries = await timeEntries.fetchByActivity(
|
||||
item.activity.cwActivityId,
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
timeEntries: entries.map((e) => e.toJson()),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Resolve CwMember info for all unique memberIds across time entries
|
||||
const allMemberIds = Array.from(
|
||||
new Set(
|
||||
activitiesWithTimeEntries.flatMap((item) =>
|
||||
item.timeEntries
|
||||
.map((te) => te.memberId)
|
||||
.filter((id): id is string => !!id),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const memberRecords = allMemberIds.length
|
||||
? await prisma.cwMember.findMany({
|
||||
where: { identifier: { in: allMemberIds } },
|
||||
select: { identifier: true, firstName: true, lastName: true, officeEmail: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const memberMap = new Map(
|
||||
memberRecords.map((m) => [
|
||||
m.identifier,
|
||||
{ name: `${m.firstName} ${m.lastName}`.trim(), email: m.officeEmail ?? null },
|
||||
]),
|
||||
);
|
||||
|
||||
// Attach member info to each time entry
|
||||
const enrichedActivities = activitiesWithTimeEntries.map((item) => ({
|
||||
...item,
|
||||
timeEntries: item.timeEntries.map((te) => ({
|
||||
...te,
|
||||
member: te.memberId ? (memberMap.get(te.memberId) ?? null) : null,
|
||||
})),
|
||||
}));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Workflow history fetched successfully.",
|
||||
{
|
||||
opportunityId: opportunity.id,
|
||||
cwOpportunityId: opportunity.cwOpportunityId,
|
||||
totalActivities: workflowActivities.length,
|
||||
activities: workflowActivities,
|
||||
totalActivities: enrichedActivities.length,
|
||||
activities: enrichedActivities,
|
||||
},
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
|
||||
@@ -248,6 +248,15 @@ const ACTION_MAP: Record<number, AvailableAction[]> = {
|
||||
requiresPermission: null,
|
||||
payloadHints: { needsRevision: "true" },
|
||||
},
|
||||
{
|
||||
action: "sendBackForRevision",
|
||||
label: "Send Back for Revision",
|
||||
targetStatuses: [
|
||||
{ key: "PendingRevision", id: OpportunityStatus.PendingRevision },
|
||||
],
|
||||
requiresNote: true,
|
||||
requiresPermission: null,
|
||||
},
|
||||
],
|
||||
|
||||
[OpportunityStatus.Active]: [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createWorkflowActivity,
|
||||
OptimaType,
|
||||
} from "../../../workflows/wf.opportunity";
|
||||
import { resolveCwProbabilityId } from "../../../modules/cw-utils/opportunities/cwProbabilityCache";
|
||||
|
||||
const createSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -46,16 +47,25 @@ export default createRoute(
|
||||
const data = createSchema.parse(body);
|
||||
const { interest, ...cwCreateData } = data;
|
||||
|
||||
try {
|
||||
const item = await opportunities.createItem(cwCreateData);
|
||||
// Resolve the CW probability reference ID for 50% (the default)
|
||||
const defaultProbabilityId = await resolveCwProbabilityId(50);
|
||||
|
||||
if (interest !== undefined) {
|
||||
await prisma.opportunity.update({
|
||||
where: { uid: item.id },
|
||||
data: { interest },
|
||||
});
|
||||
item.interest = interest;
|
||||
}
|
||||
try {
|
||||
const item = await opportunities.createItem({
|
||||
...cwCreateData,
|
||||
...(defaultProbabilityId != null
|
||||
? { probability: { id: defaultProbabilityId } }
|
||||
: {}),
|
||||
});
|
||||
|
||||
// Apply defaults: "HOT" interest and 50% probability if not explicitly provided.
|
||||
const effectiveInterest = interest !== undefined ? interest : "HOT";
|
||||
await prisma.opportunity.update({
|
||||
where: { uid: item.id },
|
||||
data: { interest: effectiveInterest, probability: 50 },
|
||||
});
|
||||
item.interest = effectiveInterest;
|
||||
item.probability = 50;
|
||||
|
||||
// Create a workflow activity for the new opportunity
|
||||
try {
|
||||
@@ -118,6 +128,7 @@ export default createRoute(
|
||||
);
|
||||
}
|
||||
|
||||
console.error("[Opportunity Create] DB write failed after CW create:", err);
|
||||
throw new GenericError({
|
||||
status: 500,
|
||||
name: "OpportunityCreateError",
|
||||
|
||||
@@ -16,6 +16,7 @@ import procurementRouter from "./routers/procurementRouter";
|
||||
import salesRouter from "./routers/salesRouter";
|
||||
import cwRouter from "./routers/cwRouter";
|
||||
import scheduleRouter from "./routers/scheduleRouter";
|
||||
import timeEntryRouter from "./routers/timeEntryRouter";
|
||||
|
||||
const app = new Hono();
|
||||
const v1 = new Hono();
|
||||
@@ -73,6 +74,7 @@ v1.route("/procurement", procurementRouter);
|
||||
v1.route("/sales", salesRouter);
|
||||
v1.route("/cw", cwRouter);
|
||||
v1.route("/schedule", scheduleRouter);
|
||||
v1.route("/time-entry", timeEntryRouter);
|
||||
app.route("/v1", v1);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -53,6 +53,7 @@ export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
|
||||
lineItemPricing: opts?.lineItemPricing,
|
||||
includeQuoteNarrative: opts?.includeQuoteNarrative,
|
||||
includeItemNarratives: opts?.includeItemNarratives,
|
||||
separateRecurringServices: opts?.separateRecurringServices,
|
||||
logoPath: opts?.logoPath,
|
||||
showPreview: true,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { timeEntries } from "../../../managers/timeEntries";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/time-entry/time-entries/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/time-entries/:identifier"],
|
||||
async (c) => {
|
||||
const entry = await timeEntries.fetch(c.req.param("identifier"));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Time entry fetched successfully!",
|
||||
entry.toJson(),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["time-entry.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { timeEntries } from "../../managers/timeEntries";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* GET /v1/time-entry/count */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/count"],
|
||||
async (c) => {
|
||||
const count = await timeEntries.count();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Time entry count fetched successfully!",
|
||||
{ count },
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["time-entry.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { timeEntries } from "../../managers/timeEntries";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* GET /v1/time-entry/time-entries?page=&rpp=&search= */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/time-entries"],
|
||||
async (c) => {
|
||||
const page = new Number(c.req.query("page") ?? 1) as number;
|
||||
const rpp = new Number(c.req.query("rpp") ?? 30) as number;
|
||||
const search = c.req.query("search");
|
||||
|
||||
const data = search
|
||||
? await timeEntries.search(search, page, rpp)
|
||||
: await timeEntries.fetchPages(page, rpp);
|
||||
|
||||
const totalRecords = search
|
||||
? (await timeEntries.search(search, 1, 999999)).length
|
||||
: await timeEntries.count();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Time entries fetched successfully!",
|
||||
data.map((e) => e.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: ["time-entry.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as count } from "./count";
|
||||
import { default as fetch } from "./[identifier]/fetch";
|
||||
import { default as fetchByMember } from "./member/fetchByMember";
|
||||
import { default as fetchByTicket } from "./ticket/fetchByTicket";
|
||||
import { default as fetchMyTimeEntries } from "./me/fetchMyTimeEntries";
|
||||
|
||||
export { count, fetch, fetchAll, fetchByMember, fetchByTicket, fetchMyTimeEntries };
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { timeEntries } from "../../../managers/timeEntries";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
|
||||
/* GET /v1/time-entry/@me?start=<ISO>&end=<ISO> */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/@me"],
|
||||
async (c) => {
|
||||
const user = c.get("user");
|
||||
|
||||
if (!user?.cwIdentifier) {
|
||||
throw new GenericError({
|
||||
name: "BadRequest",
|
||||
message:
|
||||
"Your account is not linked to a ConnectWise member. Cannot fetch time entries.",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const startParam = c.req.query("start");
|
||||
const endParam = c.req.query("end");
|
||||
|
||||
if (!startParam || !endParam) {
|
||||
throw new GenericError({
|
||||
name: "BadRequest",
|
||||
message:
|
||||
"Query params 'start' and 'end' are required (ISO 8601 date strings).",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const startDate = new Date(startParam);
|
||||
const endDate = new Date(endParam);
|
||||
|
||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||
throw new GenericError({
|
||||
name: "BadRequest",
|
||||
message: "Invalid date format. Use ISO 8601 (e.g. 2026-04-01T00:00:00Z).",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (startDate >= endDate) {
|
||||
throw new GenericError({
|
||||
name: "BadRequest",
|
||||
message: "'start' must be before 'end'.",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await timeEntries.fetchByDateRange(
|
||||
startDate,
|
||||
endDate,
|
||||
user.cwIdentifier,
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Time entries fetched successfully!",
|
||||
data.map((e) => e.toJson()),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["time-entry.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { timeEntries } from "../../../managers/timeEntries";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/time-entry/member/:memberId?page=&rpp= */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/member/:memberId"],
|
||||
async (c) => {
|
||||
const memberId = c.req.param("memberId");
|
||||
const page = new Number(c.req.query("page") ?? 1) as number;
|
||||
const rpp = new Number(c.req.query("rpp") ?? 30) as number;
|
||||
|
||||
const data = await timeEntries.fetchByMember(memberId, page, rpp);
|
||||
const totalRecords = await timeEntries.countByMember(memberId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Time entries fetched successfully!",
|
||||
data.map((e) => e.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: ["time-entry.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { timeEntries } from "../../../managers/timeEntries";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* GET /v1/time-entry/ticket/:ticketId */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/ticket/:ticketId"],
|
||||
async (c) => {
|
||||
const ticketId = parseInt(c.req.param("ticketId"), 10);
|
||||
|
||||
const data = await timeEntries.fetchByTicket(ticketId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Time entries fetched successfully!",
|
||||
data.map((e) => e.toJson()),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["time-entry.fetch.many"] }),
|
||||
);
|
||||
@@ -12,7 +12,7 @@ export default createRoute(
|
||||
async (c) => {
|
||||
const siteId = c.req.param("id");
|
||||
const body = await c.req.json();
|
||||
const schema = z.object({ companyId: z.string() }).strict();
|
||||
const schema = z.object({ companyId: z.number().int() }).strict();
|
||||
const { companyId } = schema.parse(body);
|
||||
|
||||
const site = await unifiSites.linkToCompany(siteId, companyId);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Shared PgBoss singleton — kept in its own module to break circular imports
|
||||
* between workert.ts and the worker modules that call getBoss().
|
||||
*/
|
||||
import { PgBoss } from "pg-boss";
|
||||
|
||||
function makePgBossUrl(rawUrl: string): string {
|
||||
try {
|
||||
const u = new URL(rawUrl);
|
||||
// 30-second statement timeout to prevent individual SQL queries from
|
||||
// hanging indefinitely if the DB server stops responding mid-query.
|
||||
u.searchParams.set("options", "-c statement_timeout=30000");
|
||||
return u.toString();
|
||||
} catch {
|
||||
return rawUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export const boss = new PgBoss({
|
||||
connectionString: makePgBossUrl(process.env.DATABASE_URL!),
|
||||
connectionTimeoutMillis: 15_000,
|
||||
});
|
||||
|
||||
boss.on("error", (err) => {
|
||||
console.error("[worker] PgBoss error", err);
|
||||
});
|
||||
|
||||
export function getBoss(): PgBoss {
|
||||
return boss;
|
||||
}
|
||||
@@ -74,8 +74,8 @@ export { io, engine };
|
||||
const connectWiseApi = axios.create({
|
||||
baseURL: `https://ttscw.totaltech.net/v4_6_release/apis/3.0/`,
|
||||
headers: {
|
||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN?.trim()}`,
|
||||
clientId: `${process.env.CW_CLIENT_ID?.trim()}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 30_000, // 30 s — prevents indefinite hangs on CW API
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CatalogManufacturer,
|
||||
} from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { catalogCw } from "../modules/cw-utils/procurement/catalog";
|
||||
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
@@ -129,7 +128,11 @@ export class CatalogItemController {
|
||||
* @returns {Promise<CatalogItemController>} - The updated controller
|
||||
*/
|
||||
public async refreshInventory(): Promise<CatalogItemController> {
|
||||
const onHand = await catalogCw.fetchInventoryOnHand(this.cwCatalogId);
|
||||
const result = await prisma.productInventory.aggregate({
|
||||
where: { itemId: this.cwCatalogId },
|
||||
_sum: { qtyOnHand: true },
|
||||
});
|
||||
const onHand = result._sum.qtyOnHand ?? 0;
|
||||
|
||||
if (onHand !== this.onHand) {
|
||||
await prisma.catalogItem.update({
|
||||
|
||||
@@ -274,7 +274,7 @@ export class CompanyController {
|
||||
(ci) => ci.type?.name === "Email"
|
||||
);
|
||||
return {
|
||||
id: contact.id,
|
||||
cwId: contact.id,
|
||||
firstName: contact.firstName,
|
||||
lastName: contact.lastName,
|
||||
inactive: contact.inactiveFlag ?? false,
|
||||
@@ -288,31 +288,87 @@ export class CompanyController {
|
||||
includeAddress?: boolean;
|
||||
includePrimaryContact?: boolean;
|
||||
includeAllContacts?: boolean;
|
||||
includeAllAddresses?: boolean;
|
||||
}) {
|
||||
const cw_Data: Record<string, unknown> = {};
|
||||
|
||||
if (opts?.includeAddress && this.cw_Data) {
|
||||
const addr = this.cw_Data.company;
|
||||
cw_Data.address = {
|
||||
line1: addr.addressLine1 ?? null,
|
||||
line2: addr.addressLine2 ?? null,
|
||||
if (opts?.includeAddress) {
|
||||
if (this.cw_Data) {
|
||||
const addr = this.cw_Data.company;
|
||||
cw_Data.address = {
|
||||
line1: addr.addressLine1 ?? null,
|
||||
line2: addr.addressLine2 ?? null,
|
||||
city: addr.city ?? null,
|
||||
state: addr.state ?? null,
|
||||
zip: addr.zip ?? null,
|
||||
country: addr.country?.name ?? "United States",
|
||||
};
|
||||
} else if (this._defaultAddress) {
|
||||
const addr = this._defaultAddress;
|
||||
cw_Data.address = {
|
||||
line1: addr.addressLine1 ?? null,
|
||||
line2: addr.addressLine2 ?? null,
|
||||
city: addr.city ?? null,
|
||||
state: addr.state ?? null,
|
||||
zip: addr.zipCode ?? null,
|
||||
country: addr.country ?? "United States",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (opts?.includeAllAddresses) {
|
||||
cw_Data.allAddresses = this._addresses.map((addr) => ({
|
||||
id: addr.id,
|
||||
uid: addr.uid,
|
||||
name: addr.name,
|
||||
description: addr.description ?? null,
|
||||
defaultFlag: addr.defaultFlag,
|
||||
inactiveFlag: addr.inactiveFlag,
|
||||
addressLine1: addr.addressLine1 ?? null,
|
||||
addressLine2: addr.addressLine2 ?? null,
|
||||
city: addr.city ?? null,
|
||||
state: addr.state ?? null,
|
||||
zip: addr.zip ?? null,
|
||||
country: addr.country?.name ?? "United States",
|
||||
};
|
||||
zip: addr.zipCode ?? null,
|
||||
country: addr.country ?? null,
|
||||
phone: addr.phone ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
if (opts?.includePrimaryContact && this.cw_Data?.defaultContact) {
|
||||
cw_Data.primaryContact = this._serializeContact(
|
||||
this.cw_Data.defaultContact
|
||||
);
|
||||
if (opts?.includePrimaryContact) {
|
||||
if (this.cw_Data?.defaultContact) {
|
||||
cw_Data.primaryContact = this._serializeContact(
|
||||
this.cw_Data.defaultContact
|
||||
);
|
||||
} else if (this._defaultContact) {
|
||||
const c = this._defaultContact;
|
||||
cw_Data.primaryContact = {
|
||||
cwId: c.id,
|
||||
firstName: c.firstName,
|
||||
lastName: c.lastName,
|
||||
inactive: !c.active,
|
||||
title: c.title ?? null,
|
||||
phone: c.phone ?? null,
|
||||
email: c.email ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (opts?.includeAllContacts && this.cw_Data?.allContacts) {
|
||||
cw_Data.allContacts = this.cw_Data.allContacts.map((c) =>
|
||||
this._serializeContact(c)
|
||||
);
|
||||
if (opts?.includeAllContacts) {
|
||||
if (this.cw_Data?.allContacts) {
|
||||
cw_Data.allContacts = this.cw_Data.allContacts.map((c) =>
|
||||
this._serializeContact(c)
|
||||
);
|
||||
} else if (this._contacts.length > 0) {
|
||||
cw_Data.allContacts = this._contacts.map((c) => ({
|
||||
cwId: c.id,
|
||||
firstName: c.firstName,
|
||||
lastName: c.lastName,
|
||||
inactive: !c.active,
|
||||
title: c.title ?? null,
|
||||
phone: c.phone ?? null,
|
||||
email: c.email ?? null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -37,11 +37,11 @@ async function resolveMember(identifier: string | null | undefined) {
|
||||
});
|
||||
return member
|
||||
? {
|
||||
id: member.id,
|
||||
identifier: member.identifier,
|
||||
name: `${member.firstName} ${member.lastName}`.trim(),
|
||||
cwMemberId: member.cwMemberId,
|
||||
}
|
||||
id: member.id,
|
||||
identifier: member.identifier,
|
||||
name: `${member.firstName} ${member.lastName}`.trim(),
|
||||
cwMemberId: member.cwMemberId,
|
||||
}
|
||||
: { id: null, identifier, name: identifier, cwMemberId: null };
|
||||
}
|
||||
|
||||
@@ -76,6 +76,20 @@ function mapRatingNameToInterest(
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatOpportunityContactName(
|
||||
firstName?: string | null,
|
||||
lastName?: string | null
|
||||
): string {
|
||||
const first = (firstName ?? "").trim();
|
||||
const last = (lastName ?? "").trim();
|
||||
|
||||
if (first && last.toLowerCase() === "contact") {
|
||||
return first;
|
||||
}
|
||||
|
||||
return `${first} ${last}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opportunity Controller
|
||||
*
|
||||
@@ -179,8 +193,8 @@ export class OpportunityController {
|
||||
});
|
||||
const resolvedName = user
|
||||
? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() ||
|
||||
user.login ||
|
||||
user.email
|
||||
user.login ||
|
||||
user.email
|
||||
: null;
|
||||
const name =
|
||||
resolvedName ??
|
||||
@@ -195,8 +209,8 @@ export class OpportunityController {
|
||||
constructor(
|
||||
data: Opportunity & {
|
||||
company?:
|
||||
| (Company & { contacts?: any[]; companyAddresses?: any[] })
|
||||
| null;
|
||||
| (Company & { contacts?: any[]; companyAddresses?: any[] })
|
||||
| null;
|
||||
primarySalesRep?: (User & { roles: Role[] }) | null;
|
||||
secondarySalesRep?: (User & { roles: Role[] }) | null;
|
||||
},
|
||||
@@ -208,8 +222,6 @@ export class OpportunityController {
|
||||
activities?: ActivityController[];
|
||||
}
|
||||
) {
|
||||
console.log(data.primarySalesRep);
|
||||
|
||||
// New schema: uid is the internal PK (string), id is the CW opportunity ID (Int)
|
||||
this.id = data.uid;
|
||||
this.cwOpportunityId = data.id;
|
||||
@@ -290,7 +302,7 @@ export class OpportunityController {
|
||||
| null
|
||||
| undefined;
|
||||
this.contactName = (data as any).contactName ?? (contactRel
|
||||
? `${contactRel.firstName} ${contactRel.lastName}`.trim()
|
||||
? formatOpportunityContactName(contactRel.firstName, contactRel.lastName)
|
||||
: null);
|
||||
|
||||
// Site
|
||||
@@ -501,8 +513,8 @@ export class OpportunityController {
|
||||
const hasCwPatch = Object.keys(cwPatch).length > 0;
|
||||
const cwMapped = hasCwPatch
|
||||
? OpportunityController.mapCwToDb(
|
||||
await opportunityCw.update(this.cwOpportunityId, cwPatch)
|
||||
)
|
||||
await opportunityCw.update(this.cwOpportunityId, cwPatch)
|
||||
)
|
||||
: {};
|
||||
|
||||
const mapped =
|
||||
@@ -674,14 +686,14 @@ export class OpportunityController {
|
||||
id: contact.id,
|
||||
contact: {
|
||||
id: contact.id,
|
||||
name: `${contact.firstName} ${contact.lastName}`.trim(),
|
||||
name: formatOpportunityContactName(contact.firstName, contact.lastName),
|
||||
},
|
||||
company: contact.company
|
||||
? {
|
||||
id: contact.company.id,
|
||||
identifier: null,
|
||||
name: contact.company.name,
|
||||
}
|
||||
id: contact.company.id,
|
||||
identifier: null,
|
||||
name: contact.company.name,
|
||||
}
|
||||
: null,
|
||||
role: null,
|
||||
notes: null,
|
||||
@@ -697,10 +709,10 @@ export class OpportunityController {
|
||||
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,
|
||||
}
|
||||
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,
|
||||
@@ -930,6 +942,7 @@ export class OpportunityController {
|
||||
lineItemPricing?: boolean;
|
||||
includeQuoteNarrative?: boolean;
|
||||
includeItemNarratives?: boolean;
|
||||
separateRecurringServices?: boolean;
|
||||
showPreview?: boolean; // INTERNAL ONLY
|
||||
logoPath?: string;
|
||||
metadata?: QuoteMetadata;
|
||||
@@ -938,6 +951,7 @@ export class OpportunityController {
|
||||
lineItemPricing: opts?.lineItemPricing ?? true,
|
||||
includeQuoteNarrative: opts?.includeQuoteNarrative ?? true,
|
||||
includeItemNarratives: opts?.includeItemNarratives ?? true,
|
||||
separateRecurringServices: opts?.separateRecurringServices ?? true,
|
||||
showPreview: opts?.showPreview ?? false,
|
||||
logoPath: opts?.logoPath,
|
||||
};
|
||||
@@ -1002,6 +1016,9 @@ export class OpportunityController {
|
||||
item.description || item.customerDescription || item.productDescription || "Line Item",
|
||||
unitPrice,
|
||||
narrative: shouldIncludeNarrative ? itemNarrative : undefined,
|
||||
isRecurring: options.separateRecurringServices
|
||||
? item.catalogItemIdentifier?.startsWith("RSV") ?? false
|
||||
: false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1042,7 +1059,7 @@ export class OpportunityController {
|
||||
|
||||
const quoteNarrativeField = options.includeQuoteNarrative
|
||||
? this._customFields?.find((f) => f.id === 35)?.value?.toString() ||
|
||||
undefined
|
||||
undefined
|
||||
: undefined;
|
||||
|
||||
// Fall back to the customerDescription of a QUO-Narrative product
|
||||
@@ -1054,8 +1071,6 @@ export class OpportunityController {
|
||||
quoNarrativeProduct?.customerDescription ??
|
||||
undefined;
|
||||
|
||||
console.log("[generateQuote] quoteNarrative:", quoteNarrative);
|
||||
|
||||
const companyLine = this.companyName ?? company?.name ?? "Customer Company";
|
||||
|
||||
// Only show attention if it differs from the customer name
|
||||
@@ -1108,6 +1123,7 @@ export class OpportunityController {
|
||||
},
|
||||
isPreview: options.showPreview,
|
||||
showLineItemPricing: options.lineItemPricing,
|
||||
separateRecurringServices: options.separateRecurringServices,
|
||||
metadata: opts?.metadata,
|
||||
};
|
||||
|
||||
@@ -1139,6 +1155,7 @@ export class OpportunityController {
|
||||
lineItemPricing?: boolean;
|
||||
includeQuoteNarrative?: boolean;
|
||||
includeItemNarratives?: boolean;
|
||||
separateRecurringServices?: boolean;
|
||||
logoPath?: string;
|
||||
} = {},
|
||||
user: UserController
|
||||
@@ -1147,6 +1164,7 @@ export class OpportunityController {
|
||||
lineItemPricing: opts?.lineItemPricing ?? true,
|
||||
includeQuoteNarrative: opts?.includeQuoteNarrative ?? true,
|
||||
includeItemNarratives: opts?.includeItemNarratives ?? true,
|
||||
separateRecurringServices: opts?.separateRecurringServices ?? true,
|
||||
logoPath: opts?.logoPath,
|
||||
};
|
||||
|
||||
@@ -1228,6 +1246,7 @@ export class OpportunityController {
|
||||
lineItemPricing: quoteOptions.lineItemPricing,
|
||||
includeQuoteNarrative: quoteOptions.includeQuoteNarrative,
|
||||
includeItemNarratives: quoteOptions.includeItemNarratives,
|
||||
separateRecurringServices: quoteOptions.separateRecurringServices,
|
||||
},
|
||||
|
||||
// Opportunity metadata
|
||||
@@ -1250,11 +1269,11 @@ export class OpportunityController {
|
||||
companyName: this.companyName ?? company?.name ?? null,
|
||||
primaryContact: companyJson?.cw_Data?.primaryContact
|
||||
? {
|
||||
firstName: companyJson.cw_Data.primaryContact.firstName ?? null,
|
||||
lastName: companyJson.cw_Data.primaryContact.lastName ?? null,
|
||||
email: companyJson.cw_Data.primaryContact.email ?? null,
|
||||
phone: companyJson.cw_Data.primaryContact.phone ?? null,
|
||||
}
|
||||
firstName: companyJson.cw_Data.primaryContact.firstName ?? null,
|
||||
lastName: companyJson.cw_Data.primaryContact.lastName ?? null,
|
||||
email: companyJson.cw_Data.primaryContact.email ?? null,
|
||||
phone: companyJson.cw_Data.primaryContact.phone ?? null,
|
||||
}
|
||||
: null,
|
||||
siteAddress: siteAddress.length > 0 ? siteAddress : null,
|
||||
companyAddress: companyAddress.length > 0 ? companyAddress : null,
|
||||
@@ -1462,14 +1481,17 @@ export class OpportunityController {
|
||||
public async resequenceProducts(
|
||||
orderedIds: number[]
|
||||
): Promise<ForecastProductController[]> {
|
||||
// Validate all IDs exist in the local ProductData table (the IDs the UI works with)
|
||||
// Validate all IDs exist in the local ProductData table (the IDs the UI works with).
|
||||
// Fall back to productSequence for items that were just added and haven't been
|
||||
// synced to productData yet — appendProductSequenceIds writes them immediately.
|
||||
const existingRows = await prisma.productData.findMany({
|
||||
where: { opportunityId: this.cwOpportunityId },
|
||||
select: { id: true },
|
||||
});
|
||||
const existingIds = new Set(existingRows.map((r) => r.id));
|
||||
const sequenceIds = new Set(this.productSequence);
|
||||
for (const id of orderedIds) {
|
||||
if (!existingIds.has(id)) {
|
||||
if (!existingIds.has(id) && !sequenceIds.has(id)) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ForecastItemNotFound",
|
||||
@@ -1636,6 +1658,11 @@ export class OpportunityController {
|
||||
public async deleteProduct(forecastItemId: number): Promise<void> {
|
||||
await opportunityCw.deleteProduct(this.cwOpportunityId, forecastItemId);
|
||||
|
||||
// Remove the deleted item from the local ProductData table
|
||||
await prisma.productData.deleteMany({
|
||||
where: { id: forecastItemId, opportunityId: this.cwOpportunityId },
|
||||
});
|
||||
|
||||
// Remove the deleted item from the local product sequence
|
||||
if (this.productSequence.includes(forecastItemId)) {
|
||||
const updatedSequence = this.productSequence.filter(
|
||||
@@ -1650,7 +1677,7 @@ export class OpportunityController {
|
||||
this.productSequence = updatedSequence;
|
||||
}
|
||||
|
||||
// No cache invalidation needed
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1803,13 +1830,13 @@ export class OpportunityController {
|
||||
const defaultAddr = this._company?.getDefaultAddress();
|
||||
const companyAddress = defaultAddr
|
||||
? {
|
||||
line1: defaultAddr.addressLine1,
|
||||
line2: defaultAddr.addressLine2,
|
||||
city: defaultAddr.city,
|
||||
state: defaultAddr.state,
|
||||
zip: defaultAddr.zipCode,
|
||||
country: defaultAddr.country ?? null,
|
||||
}
|
||||
line1: defaultAddr.addressLine1,
|
||||
line2: defaultAddr.addressLine2,
|
||||
city: defaultAddr.city,
|
||||
state: defaultAddr.state,
|
||||
zip: defaultAddr.zipCode,
|
||||
country: defaultAddr.country ?? null,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
@@ -1836,58 +1863,58 @@ export class OpportunityController {
|
||||
primarySalesRep:
|
||||
this.primarySalesRepIdentifier || this._primarySalesRep
|
||||
? {
|
||||
id: this.primarySalesRepCwId,
|
||||
identifier: this.primarySalesRepIdentifier,
|
||||
name:
|
||||
this._primarySalesRep?.name ??
|
||||
this.primarySalesRepName ??
|
||||
this.primarySalesRepIdentifier,
|
||||
...(this._primarySalesRep
|
||||
? { user: this._primarySalesRep.toJson({ safeReturn: true }) }
|
||||
: {}),
|
||||
}
|
||||
id: this.primarySalesRepCwId,
|
||||
identifier: this.primarySalesRepIdentifier,
|
||||
name:
|
||||
this._primarySalesRep?.name ??
|
||||
this.primarySalesRepName ??
|
||||
this.primarySalesRepIdentifier,
|
||||
...(this._primarySalesRep
|
||||
? { user: this._primarySalesRep.toJson({ safeReturn: true }) }
|
||||
: {}),
|
||||
}
|
||||
: null,
|
||||
secondarySalesRep:
|
||||
this.secondarySalesRepIdentifier || this._secondarySalesRep
|
||||
? {
|
||||
id: this.secondarySalesRepCwId,
|
||||
identifier: this.secondarySalesRepIdentifier,
|
||||
name:
|
||||
this._secondarySalesRep?.name ??
|
||||
this.secondarySalesRepName ??
|
||||
this.secondarySalesRepIdentifier,
|
||||
...(this._secondarySalesRep
|
||||
? {
|
||||
user: this._secondarySalesRep.toJson({
|
||||
safeReturn: true,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
id: this.secondarySalesRepCwId,
|
||||
identifier: this.secondarySalesRepIdentifier,
|
||||
name:
|
||||
this._secondarySalesRep?.name ??
|
||||
this.secondarySalesRepName ??
|
||||
this.secondarySalesRepIdentifier,
|
||||
...(this._secondarySalesRep
|
||||
? {
|
||||
user: this._secondarySalesRep.toJson({
|
||||
safeReturn: true,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: null,
|
||||
company: this._company
|
||||
? this._company.toJson({
|
||||
includeAllContacts: true,
|
||||
includeAddress: true,
|
||||
includePrimaryContact: false,
|
||||
})
|
||||
includeAllContacts: true,
|
||||
includeAddress: true,
|
||||
includePrimaryContact: false,
|
||||
})
|
||||
: this.companyCwId
|
||||
? { id: this.companyCwId, name: this.companyName }
|
||||
: null,
|
||||
? { id: this.companyCwId, name: this.companyName }
|
||||
: null,
|
||||
contact: this.contactCwId
|
||||
? { id: this.contactCwId, name: this.contactName }
|
||||
: null,
|
||||
site: this._siteData
|
||||
? this._siteData
|
||||
: this.siteCwId
|
||||
? { id: this.siteCwId, name: this.siteName }
|
||||
: null,
|
||||
? { id: this.siteCwId, name: this.siteName }
|
||||
: null,
|
||||
customerPO: this.customerPO,
|
||||
totalSalesTax: this.totalSalesTax,
|
||||
expectedSalesTaxRate:
|
||||
this.taxCodeRate !== null ? this.taxCodeRate * 100 : null,
|
||||
taxCodeDescription: this.taxCodeDescription,
|
||||
probability: this.probability,
|
||||
probability: this.probability != null ? { percent: this.probability } : null,
|
||||
location: this.locationCwId
|
||||
? { id: this.locationCwId, name: this.locationName }
|
||||
: null,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
TimeEntry,
|
||||
TimeEntryStatus,
|
||||
TimeEntryChargeCode,
|
||||
Company,
|
||||
ServiceTicket,
|
||||
Activity,
|
||||
Contact,
|
||||
CorporateLocation,
|
||||
} from "../../generated/prisma/client";
|
||||
|
||||
type TimeEntryWithRelations = TimeEntry & {
|
||||
status?: TimeEntryStatus | null;
|
||||
chargeCode?: TimeEntryChargeCode | null;
|
||||
company?: Company | null;
|
||||
serviceTicket?: ServiceTicket | null;
|
||||
activity?: Activity | null;
|
||||
contact?: Contact | null;
|
||||
location?: CorporateLocation | null;
|
||||
};
|
||||
|
||||
export class TimeEntryController {
|
||||
public readonly id: number;
|
||||
public readonly uid: string;
|
||||
|
||||
private _data: TimeEntryWithRelations;
|
||||
|
||||
constructor(data: TimeEntryWithRelations) {
|
||||
this.id = data.id;
|
||||
this.uid = data.uid;
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
public toJson() {
|
||||
const d = this._data;
|
||||
return {
|
||||
id: d.uid,
|
||||
cwId: d.id,
|
||||
memberId: d.memberId,
|
||||
|
||||
// ------ Time Fields ------
|
||||
dateStart: d.dateStart,
|
||||
timeStart: d.timeStart,
|
||||
timeEnd: d.timeEnd,
|
||||
|
||||
// ------ Notes ------
|
||||
notes: d.notes,
|
||||
notesMd: d.notesMd,
|
||||
internalNote: d.internalNote,
|
||||
|
||||
// ------ Hours ------
|
||||
billableHours: d.billableHours,
|
||||
actualHours: d.actualHours,
|
||||
invoicedHours: d.invoicedHours,
|
||||
deductedHours: d.deductedHours,
|
||||
|
||||
// ------ Rates ------
|
||||
hourlyRate: d.hourlyRate,
|
||||
effectiveRate: d.effectiveRate,
|
||||
|
||||
// ------ Flag Fields ------
|
||||
issueFlag: d.issueFlag,
|
||||
mergedFlag: d.mergedFlag,
|
||||
invoiceFlag: d.invoiceFlag,
|
||||
billableFlag: d.billableFlag,
|
||||
documentFlag: d.documentFlag,
|
||||
teProblemFlag: d.teProblemFlag,
|
||||
teResolutionFlag: d.teResolutionFlag,
|
||||
teInternalAnalysisFlag: d.teInternalAnalysisFlag,
|
||||
|
||||
// ------ Charge Info ------
|
||||
chargeToRecId: d.chargeToRecId,
|
||||
chargeToType: d.chargeToType,
|
||||
|
||||
// ------ Relations ------
|
||||
company: d.company
|
||||
? { id: d.company.uid, cwId: d.company.id, name: d.company.name }
|
||||
: null,
|
||||
serviceTicket: d.serviceTicket
|
||||
? {
|
||||
id: d.serviceTicket.uid,
|
||||
cwId: d.serviceTicket.id,
|
||||
summary: d.serviceTicket.summary,
|
||||
}
|
||||
: null,
|
||||
activity: d.activity
|
||||
? {
|
||||
id: d.activity.uid,
|
||||
cwId: d.activity.id,
|
||||
subject: d.activity.subject,
|
||||
}
|
||||
: null,
|
||||
contact: d.contact
|
||||
? {
|
||||
id: d.contact.uid,
|
||||
cwId: d.contact.id,
|
||||
name: `${d.contact.firstName} ${d.contact.lastName}`.trim(),
|
||||
}
|
||||
: null,
|
||||
location: d.location
|
||||
? { id: d.location.uid, cwId: d.location.id, name: d.location.name }
|
||||
: null,
|
||||
status: d.status
|
||||
? {
|
||||
id: d.status.uid,
|
||||
cwId: d.status.id,
|
||||
description: d.status.description,
|
||||
action: d.status.action,
|
||||
}
|
||||
: null,
|
||||
chargeCode: d.chargeCode
|
||||
? {
|
||||
id: d.chargeCode.uid,
|
||||
cwId: d.chargeCode.id,
|
||||
description: d.chargeCode.description,
|
||||
expenseFlag: d.chargeCode.expenseFlag,
|
||||
timeFlag: d.chargeCode.timeFlag,
|
||||
billableFlag: d.chargeCode.billableFlag,
|
||||
invoiceFlag: d.chargeCode.invoiceFlag,
|
||||
}
|
||||
: null,
|
||||
|
||||
// ------ Audit ------
|
||||
createdById: d.createdById,
|
||||
updatedById: d.updatedById,
|
||||
originalAuthorId: d.originalAuthorId,
|
||||
createdAt: d.createdAt,
|
||||
updatedAt: d.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,7 +6,8 @@ import { events } from "./modules/globalEvents";
|
||||
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
||||
import { RoleController } from "./controllers/RoleController";
|
||||
import { initializeWorkerSystem, getBoss } from "./workert";
|
||||
import { initializeWorkerSystem } from "./workert";
|
||||
import { getBoss } from "./boss-instance";
|
||||
import { WorkerQueue } from "./modules/workers/queues";
|
||||
import { enqueueIncrementalSync } from "./modules/workers/incremental-sync";
|
||||
import { startCommsServer } from "./modules/workers/coms";
|
||||
|
||||
@@ -46,7 +46,18 @@ export const opportunities = {
|
||||
|
||||
// Resolve optional local FKs — nullify any that don't exist locally yet
|
||||
// (the sync may be behind; these are all nullable in the schema)
|
||||
const [companyExists, contactExists, siteExists] = await Promise.all([
|
||||
const [
|
||||
companyExists,
|
||||
contactExists,
|
||||
siteExists,
|
||||
typeExists,
|
||||
stageExists,
|
||||
statusExists,
|
||||
locationExists,
|
||||
departmentExists,
|
||||
primaryRepExists,
|
||||
secondaryRepExists,
|
||||
] = await Promise.all([
|
||||
cwData.company?.id
|
||||
? prisma.company.findFirst({ where: { id: cwData.company.id }, select: { id: true } })
|
||||
: null,
|
||||
@@ -56,19 +67,70 @@ export const opportunities = {
|
||||
mapped.siteId != null
|
||||
? prisma.companyAddress.findFirst({ where: { id: mapped.siteId }, select: { id: true } })
|
||||
: null,
|
||||
mapped.typeId != null
|
||||
? prisma.opportunityType.findFirst({ where: { id: mapped.typeId }, select: { id: true } })
|
||||
: null,
|
||||
mapped.stageId != null
|
||||
? prisma.opportunityStage.findFirst({ where: { id: mapped.stageId }, select: { id: true } })
|
||||
: null,
|
||||
mapped.statusId != null
|
||||
? prisma.opportunityStatus.findFirst({ where: { id: mapped.statusId }, select: { id: true } })
|
||||
: null,
|
||||
mapped.locationId != null
|
||||
? prisma.corporateLocation.findFirst({ where: { id: mapped.locationId }, select: { id: true } })
|
||||
: null,
|
||||
mapped.departmentId != null
|
||||
? prisma.internalDepartment.findFirst({ where: { id: mapped.departmentId }, select: { id: true } })
|
||||
: null,
|
||||
mapped.primarySalesRepId != null
|
||||
? prisma.user.findFirst({ where: { cwIdentifier: mapped.primarySalesRepId }, select: { cwIdentifier: true } })
|
||||
: null,
|
||||
mapped.secondarySalesRepId != null
|
||||
? prisma.user.findFirst({ where: { cwIdentifier: mapped.secondarySalesRepId }, select: { cwIdentifier: true } })
|
||||
: null,
|
||||
]);
|
||||
|
||||
const companyId = companyExists?.id ?? null;
|
||||
const contactId = contactExists?.id ?? null;
|
||||
const siteId = siteExists?.id ?? null;
|
||||
const typeId = typeExists?.id ?? null;
|
||||
const stageId = stageExists?.id ?? null;
|
||||
const statusId = statusExists?.id ?? null;
|
||||
const locationId = locationExists?.id ?? null;
|
||||
const departmentId = departmentExists?.id ?? null;
|
||||
const primarySalesRepId = primaryRepExists?.cwIdentifier ?? null;
|
||||
const secondarySalesRepId = secondaryRepExists?.cwIdentifier ?? null;
|
||||
|
||||
// Strip fields returned by mapCwToDb that are not columns in the Prisma schema
|
||||
// (ratingName, ratingCwId, campaignName, primarySalesRepName, primarySalesRepIdentifier,
|
||||
// secondarySalesRepName, secondarySalesRepIdentifier, cwLastUpdated).
|
||||
// Prisma will throw a validation error if unknown fields are passed to create().
|
||||
const {
|
||||
ratingName: _ratingName,
|
||||
ratingCwId: _ratingCwId,
|
||||
campaignName: _campaignName,
|
||||
primarySalesRepName: _primarySalesRepName,
|
||||
primarySalesRepIdentifier: _primarySalesRepIdentifier,
|
||||
secondarySalesRepName: _secondarySalesRepName,
|
||||
secondarySalesRepIdentifier: _secondarySalesRepIdentifier,
|
||||
cwLastUpdated: _cwLastUpdated,
|
||||
...dbFields
|
||||
} = mapped;
|
||||
|
||||
const record = await prisma.opportunity.create({
|
||||
data: {
|
||||
id: cwData.id,
|
||||
...mapped,
|
||||
...dbFields,
|
||||
typeId,
|
||||
stageId,
|
||||
statusId,
|
||||
locationId,
|
||||
departmentId,
|
||||
companyId,
|
||||
contactId,
|
||||
siteId,
|
||||
primarySalesRepId,
|
||||
secondarySalesRepId,
|
||||
},
|
||||
include: {
|
||||
company: { include: { contacts: true, companyAddresses: true } },
|
||||
@@ -113,7 +175,7 @@ export const opportunities = {
|
||||
|
||||
const record = await prisma.opportunity.findFirst({
|
||||
where: isNumeric
|
||||
? ({ cwOpportunityId: Number(identifier) } as any)
|
||||
? { cwOpportunityId: Number(identifier) }
|
||||
: { uid: identifier as string },
|
||||
include: {
|
||||
company: { include: { contacts: true, companyAddresses: true } },
|
||||
@@ -486,4 +548,40 @@ export const opportunities = {
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete Opportunity
|
||||
*
|
||||
* Deletes an opportunity from ConnectWise and removes the corresponding
|
||||
* record (along with its associated ProductData) from the local database.
|
||||
*
|
||||
* @param identifier - The internal uid (string) or CW opportunity ID (number)
|
||||
*/
|
||||
async deleteItem(identifier: string | number): Promise<void> {
|
||||
const isNumeric =
|
||||
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||
|
||||
const record = await prisma.opportunity.findFirst({
|
||||
where: isNumeric
|
||||
? { id: Number(identifier) }
|
||||
: { uid: identifier as string },
|
||||
select: { uid: true, id: true },
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
throw new GenericError({
|
||||
message: "Opportunity not found",
|
||||
name: "OpportunityNotFound",
|
||||
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
await opportunityCw.delete(record.id);
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.productData.deleteMany({ where: { opportunityId: record.id } }),
|
||||
prisma.opportunity.delete({ where: { uid: record.uid } }),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { prisma } from "../constants";
|
||||
import { TimeEntryController } from "../controllers/TimeEntryController";
|
||||
|
||||
const timeEntryIncludes = {
|
||||
status: true,
|
||||
chargeCode: true,
|
||||
company: true,
|
||||
serviceTicket: true,
|
||||
activity: true,
|
||||
contact: true,
|
||||
location: true,
|
||||
} as const;
|
||||
|
||||
export const timeEntries = {
|
||||
async fetch(identifier: string | number): Promise<TimeEntryController> {
|
||||
const isNumeric =
|
||||
typeof identifier === "number" || /^\d+$/.test(String(identifier));
|
||||
|
||||
const entry = await prisma.timeEntry.findFirst({
|
||||
where: isNumeric
|
||||
? { id: Number(identifier) }
|
||||
: { uid: String(identifier) },
|
||||
include: timeEntryIncludes,
|
||||
});
|
||||
|
||||
if (!entry) throw new Error("Unknown time entry.");
|
||||
|
||||
return new TimeEntryController(entry);
|
||||
},
|
||||
|
||||
async count(): Promise<number> {
|
||||
return prisma.timeEntry.count();
|
||||
},
|
||||
|
||||
async fetchPages(page: number, rpp: number): Promise<TimeEntryController[]> {
|
||||
page = page.valueOf();
|
||||
rpp = rpp.valueOf();
|
||||
|
||||
const skip = (page > 1 ? page - 1 : 0) * rpp;
|
||||
const take = rpp ?? 30;
|
||||
|
||||
const data = await prisma.timeEntry.findMany({
|
||||
skip,
|
||||
take,
|
||||
include: timeEntryIncludes,
|
||||
orderBy: { dateStart: "desc" },
|
||||
});
|
||||
|
||||
return data.map((e) => new TimeEntryController(e));
|
||||
},
|
||||
|
||||
async search(
|
||||
query: string,
|
||||
page: number,
|
||||
rpp: number
|
||||
): Promise<TimeEntryController[]> {
|
||||
page = page.valueOf();
|
||||
rpp = rpp.valueOf();
|
||||
|
||||
const skip = (page > 1 ? page - 1 : 0) * rpp;
|
||||
const take = rpp ?? 30;
|
||||
|
||||
const numericQuery = parseInt(query, 10);
|
||||
|
||||
const data = await prisma.timeEntry.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ notes: { contains: query, mode: "insensitive" } },
|
||||
{ internalNote: { contains: query, mode: "insensitive" } },
|
||||
{ uid: { contains: query, mode: "insensitive" } },
|
||||
...(!isNaN(numericQuery) ? [{ id: numericQuery }] : []),
|
||||
],
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
include: timeEntryIncludes,
|
||||
orderBy: { dateStart: "desc" },
|
||||
});
|
||||
|
||||
return data.map((e) => new TimeEntryController(e));
|
||||
},
|
||||
|
||||
async fetchByMember(
|
||||
memberId: string,
|
||||
page: number,
|
||||
rpp: number
|
||||
): Promise<TimeEntryController[]> {
|
||||
page = page.valueOf();
|
||||
rpp = rpp.valueOf();
|
||||
|
||||
const skip = (page > 1 ? page - 1 : 0) * rpp;
|
||||
const take = rpp ?? 30;
|
||||
|
||||
const data = await prisma.timeEntry.findMany({
|
||||
where: { memberId },
|
||||
skip,
|
||||
take,
|
||||
include: timeEntryIncludes,
|
||||
orderBy: { dateStart: "desc" },
|
||||
});
|
||||
|
||||
return data.map((e) => new TimeEntryController(e));
|
||||
},
|
||||
|
||||
async countByMember(memberId: string): Promise<number> {
|
||||
return prisma.timeEntry.count({ where: { memberId } });
|
||||
},
|
||||
|
||||
async fetchByTicket(
|
||||
serviceTicketId: number
|
||||
): Promise<TimeEntryController[]> {
|
||||
const data = await prisma.timeEntry.findMany({
|
||||
where: { serviceTicketId },
|
||||
include: timeEntryIncludes,
|
||||
orderBy: { dateStart: "desc" },
|
||||
});
|
||||
|
||||
return data.map((e) => new TimeEntryController(e));
|
||||
},
|
||||
|
||||
async fetchByActivity(activityId: number): Promise<TimeEntryController[]> {
|
||||
const data = await prisma.timeEntry.findMany({
|
||||
where: { activityId },
|
||||
include: timeEntryIncludes,
|
||||
orderBy: { dateStart: "desc" },
|
||||
});
|
||||
|
||||
return data.map((e) => new TimeEntryController(e));
|
||||
},
|
||||
|
||||
async fetchByDateRange(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
memberId?: string
|
||||
): Promise<TimeEntryController[]> {
|
||||
const data = await prisma.timeEntry.findMany({
|
||||
where: {
|
||||
dateStart: { gte: startDate, lte: endDate },
|
||||
...(memberId ? { memberId } : {}),
|
||||
},
|
||||
include: timeEntryIncludes,
|
||||
orderBy: { dateStart: "asc" },
|
||||
});
|
||||
|
||||
return data.map((e) => new TimeEntryController(e));
|
||||
},
|
||||
};
|
||||
@@ -66,7 +66,7 @@ export const unifiSites = {
|
||||
/**
|
||||
* Fetch all UniFi site records linked to a specific company.
|
||||
*/
|
||||
async fetchByCompany(companyId: string): Promise<UnifiSite[]> {
|
||||
async fetchByCompany(companyId: number): Promise<UnifiSite[]> {
|
||||
return prisma.unifiSite.findMany({
|
||||
where: { companyId },
|
||||
});
|
||||
@@ -75,7 +75,7 @@ export const unifiSites = {
|
||||
/**
|
||||
* Link a UniFi site to a company.
|
||||
*/
|
||||
async linkToCompany(siteId: string, companyId: string): Promise<UnifiSite> {
|
||||
async linkToCompany(siteId: string, companyId: number): Promise<UnifiSite> {
|
||||
const site = await prisma.unifiSite.findFirst({ where: { id: siteId } });
|
||||
if (!site)
|
||||
throw new GenericError({
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* cwProbabilityCache
|
||||
*
|
||||
* Fetches and caches the list of ConnectWise probability dropdown options.
|
||||
* Used to resolve a numeric percent (0–100) to the CW probability reference ID
|
||||
* required when creating or updating opportunities via the REST API.
|
||||
*
|
||||
* CW endpoint: GET /sales/probabilities
|
||||
* Returns: [{ id: number, probability: number }]
|
||||
*/
|
||||
|
||||
import { connectWiseApi } from "../../../constants";
|
||||
|
||||
interface CWProbability {
|
||||
id: number;
|
||||
probability: number;
|
||||
}
|
||||
|
||||
let _cache: CWProbability[] | null = null;
|
||||
|
||||
async function fetchProbabilities(): Promise<CWProbability[]> {
|
||||
if (_cache) return _cache;
|
||||
const response = await connectWiseApi.get<CWProbability[]>(
|
||||
"/sales/probabilities",
|
||||
{ params: { pageSize: 1000 } }
|
||||
);
|
||||
_cache = response.data;
|
||||
return _cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the CW probability reference ID for a given percent value.
|
||||
*
|
||||
* Finds an exact match first; if none, returns the closest option.
|
||||
* Returns null if the probabilities list is empty.
|
||||
*/
|
||||
export async function resolveCwProbabilityId(
|
||||
percent: number
|
||||
): Promise<number | null> {
|
||||
const list = await fetchProbabilities();
|
||||
if (list.length === 0) return null;
|
||||
|
||||
// Exact match
|
||||
const exact = list.find((p) => p.probability === percent);
|
||||
if (exact) return exact.id;
|
||||
|
||||
// Closest match
|
||||
let closest = list[0]!;
|
||||
let minDiff = Math.abs(closest.probability - percent);
|
||||
for (const option of list) {
|
||||
const diff = Math.abs(option.probability - percent);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closest = option;
|
||||
}
|
||||
}
|
||||
return closest.id;
|
||||
}
|
||||
|
||||
/** Clear the cache (useful for tests or forced refresh). */
|
||||
export function clearCwProbabilityCache(): void {
|
||||
_cache = null;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CWOpportunitySummary,
|
||||
CWForecast,
|
||||
CWForecastItem,
|
||||
CWOpportunityProduct,
|
||||
CWForecastItemCreate,
|
||||
CWProcurementProduct,
|
||||
CWProcurementProductCreate,
|
||||
@@ -158,9 +159,9 @@ export const opportunityCw = {
|
||||
* Fetches the full forecast object (products, revenue summaries, totals)
|
||||
* for a given opportunity.
|
||||
*/
|
||||
fetchProducts: async (opportunityId: number): Promise<CWForecast> => {
|
||||
fetchProducts: async (opportunityId: number): Promise<CWOpportunityProduct[]> => {
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/opportunities/${opportunityId}/forecast`,
|
||||
`/procurement/products?conditions=opportunity/id=${opportunityId}&pageSize=1000`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
@@ -180,18 +181,18 @@ export const opportunityCw = {
|
||||
const items_to_add = Array.isArray(data) ? data : [data];
|
||||
const url = `/sales/opportunities/${opportunityId}/forecast`;
|
||||
|
||||
// 1. Fetch existing forecast to derive defaults & diff IDs later
|
||||
// 1. Fetch existing products to derive defaults & diff IDs later
|
||||
const existing = await opportunityCw.fetchProducts(opportunityId);
|
||||
const existingIds = new Set(
|
||||
(existing.forecastItems ?? []).map((fi) => fi.id),
|
||||
existing.map((p) => p.forecastDetailId).filter((id): id is number => id != null),
|
||||
);
|
||||
|
||||
// Derive sensible defaults from an existing item when available
|
||||
const templateItem = (existing.forecastItems ?? [])[0];
|
||||
const defaultStatus = templateItem?.status
|
||||
? { id: templateItem.status.id }
|
||||
const templateItem = existing[0];
|
||||
const defaultStatus = templateItem?.forecastStatus
|
||||
? { id: templateItem.forecastStatus.id }
|
||||
: { id: 1 };
|
||||
const defaultForecastType = templateItem?.forecastType ?? "Product";
|
||||
const defaultForecastType = "Product";
|
||||
|
||||
// 2. Build forecast items with required CW fields filled in
|
||||
const forecastItems = items_to_add.map((newItem) => ({
|
||||
@@ -234,9 +235,8 @@ export const opportunityCw = {
|
||||
forecastItemId: number,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<CWForecastItem> => {
|
||||
const forecast = await opportunityCw.fetchProducts(opportunityId);
|
||||
const items = forecast.forecastItems ?? [];
|
||||
const idx = items.findIndex((fi) => fi.id === forecastItemId);
|
||||
const items = await opportunityCw.fetchProducts(opportunityId);
|
||||
const idx = items.findIndex((p) => p.forecastDetailId === forecastItemId);
|
||||
if (idx === -1) {
|
||||
throw new Error(
|
||||
`Forecast item ${forecastItemId} not found on opportunity ${opportunityId}`,
|
||||
@@ -265,14 +265,13 @@ export const opportunityCw = {
|
||||
opportunityId: number,
|
||||
updates: Map<number, Record<string, unknown>>,
|
||||
): Promise<CWForecastItem[]> => {
|
||||
const forecast = await opportunityCw.fetchProducts(opportunityId);
|
||||
const items = forecast.forecastItems ?? [];
|
||||
const items = await opportunityCw.fetchProducts(opportunityId);
|
||||
|
||||
const operations: { op: "replace"; path: string; value: unknown }[] = [];
|
||||
const touchedIndices: number[] = [];
|
||||
|
||||
for (const [itemId, changes] of updates) {
|
||||
const idx = items.findIndex((fi) => fi.id === itemId);
|
||||
const idx = items.findIndex((p) => p.forecastDetailId === itemId);
|
||||
if (idx === -1) {
|
||||
throw new Error(
|
||||
`Forecast item ${itemId} not found on opportunity ${opportunityId}`,
|
||||
@@ -304,20 +303,18 @@ export const opportunityCw = {
|
||||
*/
|
||||
deleteProduct: async (
|
||||
opportunityId: number,
|
||||
forecastItemId: number,
|
||||
productId: number,
|
||||
): Promise<void> => {
|
||||
const forecast = await opportunityCw.fetchProducts(opportunityId);
|
||||
const items = forecast.forecastItems ?? [];
|
||||
|
||||
const filtered = items.filter((fi) => fi.id !== forecastItemId);
|
||||
if (filtered.length === items.length) {
|
||||
const products = await opportunityCw.fetchProducts(opportunityId);
|
||||
const found = products.find((p) => p.id === productId);
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Forecast item ${forecastItemId} not found on opportunity ${opportunityId}`,
|
||||
`Product ${productId} not found on opportunity ${opportunityId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const url = `/sales/opportunities/${opportunityId}/forecast`;
|
||||
await connectWiseApi.put(url, { ...forecast, forecastItems: filtered });
|
||||
const filtered = products.filter((p) => p.id !== productId);
|
||||
const url = `/procurement/products/${productId}`;
|
||||
await connectWiseApi.delete(url);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -272,6 +272,7 @@ export interface CWOpportunityUpdate {
|
||||
stage?: { id: number };
|
||||
status?: { id: number };
|
||||
priority?: { id: number };
|
||||
probability?: { id: number };
|
||||
campaign?: { id: number };
|
||||
primarySalesRep?: { id: number };
|
||||
secondarySalesRep?: { id: number } | null;
|
||||
@@ -296,6 +297,7 @@ export interface CWOpportunityCreate {
|
||||
stage?: { id: number };
|
||||
status?: { id: number };
|
||||
priority?: { id: number };
|
||||
probability?: { id: number };
|
||||
campaign?: { id: number };
|
||||
secondarySalesRep?: { id: number } | null;
|
||||
site?: { id: number } | null;
|
||||
@@ -311,3 +313,69 @@ export interface CWOpportunitySummary {
|
||||
id: number;
|
||||
_info?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CWOpportunityProduct {
|
||||
id: number;
|
||||
catalogItem?: {
|
||||
id: number;
|
||||
identifier: string;
|
||||
_info?: Record<string, string>;
|
||||
};
|
||||
description: string;
|
||||
sequenceNumber: number;
|
||||
quantity: number;
|
||||
unitOfMeasure?: {
|
||||
id: number;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
};
|
||||
price: number;
|
||||
cost: number;
|
||||
extPrice: number;
|
||||
extCost: number;
|
||||
discount: number;
|
||||
margin: number;
|
||||
billableOption: string;
|
||||
locationId: number;
|
||||
location?: CWReference;
|
||||
businessUnitId: number;
|
||||
businessUnit?: CWReference;
|
||||
vendor?: {
|
||||
id: number;
|
||||
identifier: string;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
};
|
||||
vendorSku?: string;
|
||||
taxableFlag: boolean;
|
||||
dropshipFlag: boolean;
|
||||
specialOrderFlag: boolean;
|
||||
phaseProductFlag: boolean;
|
||||
cancelledFlag: boolean;
|
||||
quantityCancelled: number;
|
||||
customerDescription: string;
|
||||
productSuppliedFlag: boolean;
|
||||
subContractorAmountLimit: number;
|
||||
opportunity?: {
|
||||
id: number;
|
||||
name: string;
|
||||
_info?: Record<string, string>;
|
||||
};
|
||||
calculatedPriceFlag: boolean;
|
||||
calculatedCostFlag: boolean;
|
||||
forecastDetailId?: number;
|
||||
taxCode?: CWReference;
|
||||
listPrice?: number;
|
||||
company?: CWCompanyReference;
|
||||
forecastStatus?: CWReference;
|
||||
productClass: string;
|
||||
needToPurchaseFlag: boolean;
|
||||
minimumStockFlag: boolean;
|
||||
poApprovedFlag: boolean;
|
||||
uom?: string;
|
||||
customFields?: CWCustomField[];
|
||||
_info?: {
|
||||
lastUpdated: string;
|
||||
updatedBy: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface QuoteLineItem {
|
||||
description: string;
|
||||
unitPrice: number;
|
||||
narrative?: string;
|
||||
isRecurring?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomerInfo {
|
||||
@@ -60,6 +61,7 @@ export interface QuoteData {
|
||||
quoteNarrative?: string;
|
||||
isPreview?: boolean;
|
||||
showLineItemPricing?: boolean;
|
||||
separateRecurringServices?: boolean;
|
||||
metadata?: QuoteMetadata;
|
||||
}
|
||||
|
||||
@@ -185,7 +187,16 @@ export async function generateQuote(
|
||||
logoPath = DEFAULT_LOGO_PATH
|
||||
): Promise<Buffer> {
|
||||
const t: QuoteTheme = { ...DEFAULT_THEME, ...theme };
|
||||
const subTotal = data.lineItems.reduce(
|
||||
|
||||
const separateRecurring = data.separateRecurringServices ?? false;
|
||||
const regularItems = separateRecurring
|
||||
? data.lineItems.filter((item) => !item.isRecurring)
|
||||
: data.lineItems;
|
||||
const recurringItems = separateRecurring
|
||||
? data.lineItems.filter((item) => item.isRecurring)
|
||||
: [];
|
||||
|
||||
const subTotal = regularItems.reduce(
|
||||
(sum, item) => sum + item.qty * item.unitPrice,
|
||||
0
|
||||
);
|
||||
@@ -196,13 +207,23 @@ export async function generateQuote(
|
||||
|
||||
const showPricing = data.showLineItemPricing ?? false;
|
||||
|
||||
const discountTotal = data.lineItems.reduce((sum, item) => {
|
||||
// Check discounts across all items so both tables share the same column structure
|
||||
const allDiscountTotal = data.lineItems.reduce((sum, item) => {
|
||||
const lineTotal = item.qty * item.unitPrice;
|
||||
return lineTotal < 0 ? sum + lineTotal : sum;
|
||||
}, 0);
|
||||
const hasDiscounts = discountTotal < 0;
|
||||
const discountTotal = regularItems.reduce((sum, item) => {
|
||||
const lineTotal = item.qty * item.unitPrice;
|
||||
return lineTotal < 0 ? sum + lineTotal : sum;
|
||||
}, 0);
|
||||
const hasDiscounts = allDiscountTotal < 0;
|
||||
const showDiscount = !showPricing && hasDiscounts;
|
||||
|
||||
const recurringTotal = recurringItems.reduce(
|
||||
(sum, item) => sum + item.qty * item.unitPrice,
|
||||
0
|
||||
);
|
||||
|
||||
const tableHeader = [
|
||||
{ text: "Qty", style: "thCell", alignment: "center" },
|
||||
{ text: "Description", style: "thCell" },
|
||||
@@ -218,57 +239,61 @@ export async function generateQuote(
|
||||
|
||||
const colCount = showPricing ? 4 : showDiscount ? 3 : 2;
|
||||
|
||||
const tableRows: Record<string, unknown>[][] = [];
|
||||
for (const item of data.lineItems) {
|
||||
// Build the description cell — stack description + narrative so they
|
||||
// are a single cell and pdfmake never splits them across pages.
|
||||
const descriptionCell: Record<string, unknown> = item.narrative
|
||||
? {
|
||||
stack: [
|
||||
{ text: item.description, style: "tdCell" },
|
||||
{
|
||||
text: item.narrative,
|
||||
style: "narrative",
|
||||
margin: [0, 2, 8, 0],
|
||||
},
|
||||
],
|
||||
}
|
||||
: { text: item.description, style: "tdCell" };
|
||||
function buildTableRows(items: QuoteLineItem[]): Record<string, unknown>[][] {
|
||||
const rows: Record<string, unknown>[][] = [];
|
||||
for (const item of items) {
|
||||
const descriptionCell: Record<string, unknown> = item.narrative
|
||||
? {
|
||||
stack: [
|
||||
{ text: item.description, style: "tdCell" },
|
||||
{
|
||||
text: item.narrative,
|
||||
style: "narrative",
|
||||
margin: [0, 2, 8, 0],
|
||||
},
|
||||
],
|
||||
}
|
||||
: { text: item.description, style: "tdCell" };
|
||||
|
||||
tableRows.push([
|
||||
{ text: String(item.qty), style: "tdCell", alignment: "center" },
|
||||
descriptionCell,
|
||||
...(showPricing
|
||||
? [
|
||||
{
|
||||
text: fmtMoney(item.unitPrice),
|
||||
style: "tdCell",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
},
|
||||
{
|
||||
text: fmtMoney(item.qty * item.unitPrice),
|
||||
style: "tdCell",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
},
|
||||
]
|
||||
: showDiscount
|
||||
? [
|
||||
{
|
||||
text:
|
||||
item.qty * item.unitPrice < 0
|
||||
? fmtMoney(item.qty * item.unitPrice)
|
||||
: "",
|
||||
style: "tdCell",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
rows.push([
|
||||
{ text: String(item.qty), style: "tdCell", alignment: "center" },
|
||||
descriptionCell,
|
||||
...(showPricing
|
||||
? [
|
||||
{
|
||||
text: fmtMoney(item.unitPrice),
|
||||
style: "tdCell",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
},
|
||||
{
|
||||
text: fmtMoney(item.qty * item.unitPrice),
|
||||
style: "tdCell",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
},
|
||||
]
|
||||
: showDiscount
|
||||
? [
|
||||
{
|
||||
text:
|
||||
item.qty * item.unitPrice < 0
|
||||
? fmtMoney(item.qty * item.unitPrice)
|
||||
: "",
|
||||
style: "tdCell",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const tableRows = buildTableRows(regularItems);
|
||||
const recurringTableRows = buildTableRows(recurringItems);
|
||||
|
||||
const headerImage = logoDataUrl
|
||||
? { image: logoDataUrl, width: 200 }
|
||||
: {
|
||||
@@ -731,64 +756,157 @@ export async function generateQuote(
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const signatureDateBlock = {
|
||||
margin: [0, 40, 0, 0],
|
||||
columns: [
|
||||
{
|
||||
width: "50%",
|
||||
stack: [
|
||||
{
|
||||
margin: [0, 40, 0, 0],
|
||||
columns: [
|
||||
canvas: [
|
||||
{
|
||||
width: "50%",
|
||||
stack: [
|
||||
{
|
||||
canvas: [
|
||||
{
|
||||
type: "line",
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 220,
|
||||
y2: 0,
|
||||
lineWidth: 0.75,
|
||||
lineColor: "#999",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Authorized Signature",
|
||||
fontSize: 7,
|
||||
color: "#888",
|
||||
margin: [0, 3, 0, 0],
|
||||
},
|
||||
],
|
||||
type: "line",
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 220,
|
||||
y2: 0,
|
||||
lineWidth: 0.75,
|
||||
lineColor: "#999",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Authorized Signature",
|
||||
fontSize: 7,
|
||||
color: "#888",
|
||||
margin: [0, 3, 0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
width: "50%",
|
||||
stack: [
|
||||
{
|
||||
canvas: [
|
||||
{
|
||||
width: "50%",
|
||||
stack: [
|
||||
{
|
||||
canvas: [
|
||||
type: "line",
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 160,
|
||||
y2: 0,
|
||||
lineWidth: 0.75,
|
||||
lineColor: "#999",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Date",
|
||||
fontSize: 7,
|
||||
color: "#888",
|
||||
margin: [0, 3, 0, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Inject recurring services section into content if needed
|
||||
if (separateRecurring && recurringItems.length > 0) {
|
||||
const tableWidths = showPricing
|
||||
? [40, "*", 75, 75]
|
||||
: showDiscount
|
||||
? [40, "*", 75]
|
||||
: [40, "*"];
|
||||
|
||||
(docDefinition as any).content.push(
|
||||
{ ...hr(t.accent, 1), margin: [0, 18, 0, 0] },
|
||||
{
|
||||
text: "RECURRING SERVICES",
|
||||
style: "sectionTitle",
|
||||
margin: [0, 8, 0, 0],
|
||||
},
|
||||
{
|
||||
margin: [0, 6, 0, 0],
|
||||
table: {
|
||||
headerRows: 1,
|
||||
dontBreakRows: true,
|
||||
widths: tableWidths,
|
||||
body: [tableHeader, ...recurringTableRows],
|
||||
},
|
||||
layout: {
|
||||
fillColor: (rowIndex: number) => {
|
||||
if (rowIndex === 0) return t.headerBg;
|
||||
return rowIndex % 2 === 0 ? ROW_ALT : null;
|
||||
},
|
||||
hLineWidth: (i: number, node: { table: { body: unknown[] } }) => {
|
||||
if (i === 0 || i === 1) return 0;
|
||||
if (i === node.table.body.length) return 1;
|
||||
return 0.5;
|
||||
},
|
||||
vLineWidth: () => 0,
|
||||
hLineColor: (i: number, node: { table: { body: unknown[] } }) =>
|
||||
i === node.table.body.length ? t.headerBg : "#E8E0D0",
|
||||
paddingLeft: (col: number) => (col === 0 ? 6 : 8),
|
||||
paddingRight: () => 8,
|
||||
paddingTop: () => 4,
|
||||
paddingBottom: () => 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
unbreakable: true,
|
||||
stack: [
|
||||
{
|
||||
margin: [0, 6, 0, 0],
|
||||
columns: [
|
||||
{ width: "*", text: "" },
|
||||
{
|
||||
width: 250,
|
||||
table: {
|
||||
widths: ["*", 110],
|
||||
body: [
|
||||
[
|
||||
{
|
||||
type: "line",
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 160,
|
||||
y2: 0,
|
||||
lineWidth: 0.75,
|
||||
lineColor: "#999",
|
||||
text: "Monthly Total",
|
||||
style: "totalFinalLabel",
|
||||
fillColor: t.headerBg,
|
||||
margin: [10, 8, 6, 8],
|
||||
border: [false, false, false, false],
|
||||
},
|
||||
{
|
||||
text: fmt(recurringTotal),
|
||||
style: "totalFinalValue",
|
||||
alignment: "right",
|
||||
noWrap: true,
|
||||
fillColor: t.brandLight,
|
||||
margin: [6, 7, 8, 7],
|
||||
border: [false, false, false, false],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Date",
|
||||
fontSize: 7,
|
||||
color: "#888",
|
||||
margin: [0, 3, 0, 0],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
layout: {
|
||||
hLineWidth: () => 0,
|
||||
vLineWidth: () => 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
signatureDateBlock
|
||||
);
|
||||
} else {
|
||||
(docDefinition as any).content[
|
||||
(docDefinition as any).content.length - 1
|
||||
].stack.push(signatureDateBlock);
|
||||
}
|
||||
|
||||
footer: (currentPage: number, pageCount: number) => ({
|
||||
(docDefinition as any).footer = (currentPage: number, pageCount: number) => ({
|
||||
margin: [0, 0, 0, 0],
|
||||
stack: [
|
||||
{
|
||||
@@ -827,8 +945,7 @@ export async function generateQuote(
|
||||
style: "disclaimer",
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const maybeDoc = printer.createPdfKitDocument(docDefinition as never) as any;
|
||||
const pdfDoc =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBoss } from "../../workert";
|
||||
import { getBoss } from "../../boss-instance";
|
||||
import { WorkerQueue } from "./queues";
|
||||
|
||||
/**
|
||||
|
||||
@@ -568,6 +568,13 @@ export const PERMISSION_NODES = {
|
||||
usedIn: ["src/api/sales/opportunities/[id]/quotes/commit.ts"],
|
||||
dependencies: ["sales.opportunity.fetch"],
|
||||
},
|
||||
{
|
||||
node: "sales.opportunity.quote.commit.backgenerate",
|
||||
description:
|
||||
"Generate a quote on an opportunity that is in a workflow state other than New or Active (e.g. PendingWon, QuoteSent). Requires sales.opportunity.quote.commit as a base.",
|
||||
usedIn: ["src/api/sales/opportunities/[id]/quotes/commit.ts"],
|
||||
dependencies: ["sales.opportunity.quote.commit"],
|
||||
},
|
||||
{
|
||||
node: "sales.opportunity.quote.preview",
|
||||
description:
|
||||
|
||||
+32
-22
@@ -1,13 +1,7 @@
|
||||
import { PgBoss } from "pg-boss";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { WorkerQueue } from "./modules/workers/queues";
|
||||
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||
|
||||
const boss = new PgBoss(process.env.DATABASE_URL!);
|
||||
|
||||
boss.on("error", (err) => {
|
||||
console.error("[worker] PgBoss error", err);
|
||||
});
|
||||
import { boss, getBoss } from "./boss-instance";
|
||||
|
||||
let bossStartPromise: Promise<void> | null = null;
|
||||
let reservationQueueReady = false;
|
||||
@@ -111,19 +105,25 @@ export async function reserveWorkerId(queueType: WorkerQueue): Promise<string> {
|
||||
|
||||
async function ensureDalpuriSyncQueue(): Promise<void> {
|
||||
try {
|
||||
console.log("[worker] Creating DALPURI_FULL_SYNC queue...");
|
||||
await boss.createQueue(WorkerQueue.DALPURI_FULL_SYNC);
|
||||
} catch {
|
||||
// Queue may already exist; ignore to keep this idempotent.
|
||||
console.log("[worker] DALPURI_FULL_SYNC queue ready");
|
||||
} catch (err) {
|
||||
console.log("[worker] DALPURI_FULL_SYNC queue already exists (or error):", (err as Error).message);
|
||||
}
|
||||
try {
|
||||
console.log("[worker] Creating DALPURI_INCREMENTAL_SYNC queue...");
|
||||
await boss.createQueue(WorkerQueue.DALPURI_INCREMENTAL_SYNC);
|
||||
} catch {
|
||||
// Queue may already exist; ignore to keep this idempotent.
|
||||
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue ready");
|
||||
} catch (err) {
|
||||
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue already exists (or error):", (err as Error).message);
|
||||
}
|
||||
try {
|
||||
console.log("[worker] Creating REFRESH_SALES_METRICS queue...");
|
||||
await boss.createQueue(WorkerQueue.REFRESH_SALES_METRICS);
|
||||
} catch {
|
||||
// Queue may already exist; ignore to keep this idempotent.
|
||||
console.log("[worker] REFRESH_SALES_METRICS queue ready");
|
||||
} catch (err) {
|
||||
console.log("[worker] REFRESH_SALES_METRICS queue already exists (or error):", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,14 +138,6 @@ export async function initializeWorkerSystem(): Promise<void> {
|
||||
console.log("[worker] Worker system initialized - ready for job enqueueing");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PgBoss instance for direct job enqueueing.
|
||||
* Must call initializeWorkerSystem() first.
|
||||
*/
|
||||
export function getBoss(): PgBoss {
|
||||
return boss;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
// if (Bun.env.NODE_ENV === "development") {
|
||||
// setupEventDebugger({ processLabel: "WORKER" });
|
||||
@@ -155,14 +147,32 @@ if (import.meta.main) {
|
||||
console.log(
|
||||
`[worker] Connecting to PgBoss on DATABASE_URL and SocketIO on ${process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671"}`
|
||||
);
|
||||
console.log(`[worker] DATABASE_URL set: ${!!process.env.DATABASE_URL}`);
|
||||
|
||||
// Ensure PgBoss is connected and queues exist
|
||||
await ensureBossStarted();
|
||||
console.log("[worker] Starting PgBoss...");
|
||||
try {
|
||||
await Promise.race([
|
||||
ensureBossStarted(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("boss.start() timed out after 30s")), 30_000)
|
||||
),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error("[worker] FATAL: PgBoss failed to start:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("[worker] PgBoss started successfully");
|
||||
console.log("[worker] Ensuring sync queues...");
|
||||
await ensureDalpuriSyncQueue();
|
||||
console.log("[worker] Sync queues ready");
|
||||
|
||||
// Register job handler for DALPURI_FULL_SYNC
|
||||
console.log("[worker] Importing sync-manager...");
|
||||
const { enqueueDalpuriFullSync } = await import("./modules/workers/sync-manager");
|
||||
console.log("[worker] Importing dalpuri-sync...");
|
||||
const { executeIncrementalSync } = await import("./modules/workers/dalpuri-sync");
|
||||
console.log("[worker] Importing incremental-sync...");
|
||||
const { enqueueIncrementalSync } = await import("./modules/workers/incremental-sync");
|
||||
await boss.work(WorkerQueue.DALPURI_FULL_SYNC, async () => {
|
||||
const socket = await ensureManagerSocketReady();
|
||||
|
||||
@@ -123,6 +123,7 @@ export const OptimaType = {
|
||||
Revision: "Revision",
|
||||
Finalized: "Finalized",
|
||||
Converted: "Converted",
|
||||
ScheduleEntry: "Schedule Entry",
|
||||
} as const;
|
||||
|
||||
/** CW custom field ID for the QuoteID field on activities. */
|
||||
@@ -131,6 +132,9 @@ const QUOTE_ID_FIELD_ID = 48;
|
||||
/** CW custom field ID for the Close Date field on activities. */
|
||||
const CLOSE_DATE_FIELD_ID = 49;
|
||||
|
||||
/** CW custom field ID for the Parent Activity field on activities. */
|
||||
export const PARENT_ACTIVITY_FIELD_ID = 50;
|
||||
|
||||
/**
|
||||
* Optima_Type values whose activities should remain Open until the
|
||||
* next workflow transition closes them automatically.
|
||||
@@ -139,6 +143,7 @@ const STAYS_OPEN_TYPES = new Set<OptimaTypeValue>([
|
||||
OptimaType.OpportunitySetup,
|
||||
OptimaType.OpportunityReview,
|
||||
OptimaType.Revision,
|
||||
OptimaType.ScheduleEntry,
|
||||
]);
|
||||
|
||||
export type OptimaTypeValue =
|
||||
@@ -151,7 +156,8 @@ export type OptimaTypeValue =
|
||||
| typeof OptimaType.QuoteGenerated
|
||||
| typeof OptimaType.Revision
|
||||
| typeof OptimaType.Finalized
|
||||
| typeof OptimaType.Converted;
|
||||
| typeof OptimaType.Converted
|
||||
| typeof OptimaType.ScheduleEntry;
|
||||
|
||||
/** Permission nodes required by gated transitions. */
|
||||
export const WorkflowPermissions = {
|
||||
@@ -210,6 +216,7 @@ const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
|
||||
OpportunityStatus.PendingWon,
|
||||
OpportunityStatus.PendingLost,
|
||||
OpportunityStatus.Active,
|
||||
OpportunityStatus.PendingRevision, // needs revision
|
||||
OpportunityStatus.InternalReview, // cold automation only
|
||||
]),
|
||||
|
||||
@@ -219,6 +226,7 @@ const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
|
||||
OpportunityStatus.PendingLost,
|
||||
OpportunityStatus.Active,
|
||||
OpportunityStatus.InternalReview, // cold automation only
|
||||
OpportunityStatus.PendingRevision, // send back for revision
|
||||
]),
|
||||
|
||||
[OpportunityStatus.Active]: new Set([
|
||||
@@ -317,7 +325,7 @@ export interface SendQuotePayload extends BaseActionPayload {
|
||||
|
||||
/**
|
||||
* Quote needs revision.
|
||||
* Creates a revision activity and transitions to Active.
|
||||
* Creates a revision activity and transitions to PendingRevision.
|
||||
*/
|
||||
needsRevision?: boolean;
|
||||
}
|
||||
@@ -342,6 +350,25 @@ export interface ResurrectPayload extends BaseActionPayload {
|
||||
/** Begin revision from PendingRevision → Active. */
|
||||
export interface BeginRevisionPayload extends BaseActionPayload {}
|
||||
|
||||
/** Send back for revision from ConfirmedQuote → PendingRevision. */
|
||||
export interface SendBackForRevisionPayload extends BaseActionPayload {
|
||||
note: string; // required
|
||||
}
|
||||
|
||||
/** Create a Schedule Entry activity (no status change). */
|
||||
export interface CreateScheduleEntryPayload extends BaseActionPayload {
|
||||
/** Activity type value: Follow-Up, Appointment, or Admin. */
|
||||
activityTypeValue: "Follow-Up" | "Appointment" | "Admin";
|
||||
/** ISO-8601 due date. */
|
||||
dueDate?: string;
|
||||
/** ISO-8601 start time. */
|
||||
startTime?: string;
|
||||
/** ISO-8601 end time. */
|
||||
endTime?: string;
|
||||
/** Optional notes for the schedule entry. */
|
||||
note?: string;
|
||||
}
|
||||
|
||||
/** Re-send from Active → QuoteSent. */
|
||||
export interface ResendQuotePayload extends SendQuotePayload {}
|
||||
|
||||
@@ -371,7 +398,9 @@ export type WorkflowAction =
|
||||
| { action: "beginRevision"; payload: BeginRevisionPayload }
|
||||
| { action: "resendQuote"; payload: ResendQuotePayload }
|
||||
| { action: "cancel"; payload: CancelPayload }
|
||||
| { action: "reopen"; payload: ReopenPayload };
|
||||
| { action: "reopen"; payload: ReopenPayload }
|
||||
| { action: "sendBackForRevision"; payload: SendBackForRevisionPayload }
|
||||
| { action: "createScheduleEntry"; payload: CreateScheduleEntryPayload };
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Result
|
||||
@@ -439,11 +468,11 @@ function ok(
|
||||
|
||||
/**
|
||||
* Build the `customFields` array for a CW activity with Optima_Type set,
|
||||
* and optionally a QuoteID.
|
||||
* and optionally a QuoteID, CloseDate, or ParentActivity.
|
||||
*/
|
||||
function buildCustomFields(
|
||||
optimaType: OptimaTypeValue,
|
||||
opts?: { quoteId?: string; closeDate?: string },
|
||||
opts?: { quoteId?: string; closeDate?: string; parentActivityCwId?: number },
|
||||
) {
|
||||
const fields: any[] = [
|
||||
{
|
||||
@@ -478,6 +507,17 @@ function buildCustomFields(
|
||||
});
|
||||
}
|
||||
|
||||
if (opts?.parentActivityCwId != null) {
|
||||
fields.push({
|
||||
id: PARENT_ACTIVITY_FIELD_ID,
|
||||
caption: "Parent_Activity",
|
||||
type: "Text",
|
||||
entryMethod: "EntryField",
|
||||
numberOfDecimals: 0,
|
||||
value: String(opts.parentActivityCwId),
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@@ -494,6 +534,7 @@ export async function createWorkflowActivity(opts: {
|
||||
quoteId?: string;
|
||||
dateStart?: string;
|
||||
dateEnd?: string;
|
||||
parentActivityCwId?: number | null;
|
||||
}): Promise<ActivityController> {
|
||||
const shouldStayOpen = STAYS_OPEN_TYPES.has(opts.optimaType);
|
||||
|
||||
@@ -517,6 +558,7 @@ export async function createWorkflowActivity(opts: {
|
||||
value: buildCustomFields(opts.optimaType, {
|
||||
quoteId: opts.quoteId,
|
||||
closeDate: now,
|
||||
parentActivityCwId: opts.parentActivityCwId ?? undefined,
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -533,6 +575,38 @@ export async function createWorkflowActivity(opts: {
|
||||
return patched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the parent activity CW ID for a newly generated quote activity.
|
||||
*
|
||||
* Finds the most recently created workflow activity (by CW ID descending) for
|
||||
* the opportunity, excluding QuoteGenerated and ScheduleEntry types. This
|
||||
* ensures the quote activity is nested under the current workflow state's
|
||||
* activity regardless of whether that activity is open or closed.
|
||||
*/
|
||||
export async function resolveQuoteParentActivityCwId(
|
||||
opportunityCwId: number,
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
const existingActivities = await activityCw.fetchByOpportunityDirect(opportunityCwId);
|
||||
// Sort descending by CW id so the most recently created comes first
|
||||
const sorted = [...existingActivities].sort((a, b) => (b.id ?? 0) - (a.id ?? 0));
|
||||
for (const raw of sorted) {
|
||||
const optimaField = raw.customFields?.find(
|
||||
(f: any) => f.id === OptimaType.FIELD_ID,
|
||||
);
|
||||
if (!optimaField?.value) continue;
|
||||
// Skip QuoteGenerated and ScheduleEntry — these should not be parents
|
||||
if (optimaField.value === OptimaType.QuoteGenerated) continue;
|
||||
if (optimaField.value === OptimaType.ScheduleEntry) continue;
|
||||
return raw.id;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn(`[Workflow:QuoteParent] Could not resolve parent activity: ${err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle optional time entry: submit to CW if timeStart and timeEnd are provided.
|
||||
*/
|
||||
@@ -1083,7 +1157,7 @@ export async function transitionToQuoteSent(
|
||||
return ok(currentStatus, targetStatus, activities);
|
||||
}
|
||||
|
||||
// ── needsRevision flag → Active ────────────────────────────────────
|
||||
// ── needsRevision flag → Active ──────────────────────────────────────
|
||||
if (payload.needsRevision) {
|
||||
const targetStatus = OpportunityStatus.Active;
|
||||
|
||||
@@ -1518,6 +1592,157 @@ export async function beginRevision(
|
||||
return ok(currentStatus, targetStatus, activities);
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfirmedQuote → PendingRevision
|
||||
*
|
||||
* Sends the opportunity back for revision from ConfirmedQuote.
|
||||
* Requires a mandatory note explaining why.
|
||||
*/
|
||||
export async function sendBackForRevision(
|
||||
opportunity: OpportunityController,
|
||||
user: WorkflowUser,
|
||||
payload: SendBackForRevisionPayload,
|
||||
): Promise<WorkflowResult> {
|
||||
const currentStatus = opportunity.statusCwId;
|
||||
if (currentStatus == null) return fail("Opportunity has no current status.");
|
||||
|
||||
const noteErr = assertNotePresent(payload.note);
|
||||
if (noteErr) return fail(noteErr, currentStatus);
|
||||
|
||||
const targetStatus = OpportunityStatus.PendingRevision;
|
||||
|
||||
const transErr = assertTransitionAllowed(currentStatus, targetStatus);
|
||||
if (transErr) return fail(transErr, currentStatus);
|
||||
|
||||
const activity = await createWorkflowActivity({
|
||||
name: `[Workflow] Sent back for revision — ${opportunity.name}`,
|
||||
opportunityCwId: opportunity.cwOpportunityId,
|
||||
companyCwId: opportunity.companyCwId,
|
||||
assignToCwMemberId: user.cwMemberId,
|
||||
notes: payload.note,
|
||||
optimaType: OptimaType.Revision,
|
||||
});
|
||||
|
||||
await syncOpportunityStatus({
|
||||
opportunityId: opportunity.cwOpportunityId,
|
||||
statusCwId: targetStatus,
|
||||
});
|
||||
|
||||
await handleTimeEntry(
|
||||
activity.cwActivityId,
|
||||
user.cwMemberId,
|
||||
payload,
|
||||
payload.note,
|
||||
);
|
||||
|
||||
return ok(currentStatus, targetStatus, [activity]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Schedule Entry activity without changing the opportunity status.
|
||||
*
|
||||
* Schedule Entry activities stay open until time is logged against them.
|
||||
*/
|
||||
export async function createScheduleEntry(
|
||||
opportunity: OpportunityController,
|
||||
user: WorkflowUser,
|
||||
payload: CreateScheduleEntryPayload,
|
||||
): Promise<WorkflowResult> {
|
||||
const currentStatus = opportunity.statusCwId;
|
||||
if (currentStatus == null) return fail("Opportunity has no current status.");
|
||||
|
||||
// CW activities require ISO-8601 without milliseconds, e.g. "2026-04-19T20:15:00Z"
|
||||
const toCwDateTime = (iso: string): string => iso.replace(/\.\d+Z$/, "Z");
|
||||
|
||||
const dateStart = payload.startTime
|
||||
? toCwDateTime(payload.startTime)
|
||||
: payload.dueDate
|
||||
? toCwDateTime(payload.dueDate)
|
||||
: undefined;
|
||||
const dateEnd = payload.endTime ? toCwDateTime(payload.endTime) : undefined;
|
||||
|
||||
// Find the currently open workflow activity (OpportunitySetup, OpportunityReview,
|
||||
// or Revision) to use as the parent for this schedule entry.
|
||||
let parentActivityCwId: number | null = null;
|
||||
try {
|
||||
const existingActivities = await activityCw.fetchByOpportunityDirect(
|
||||
opportunity.cwOpportunityId,
|
||||
);
|
||||
for (const raw of existingActivities) {
|
||||
if (raw.status?.id === 2) continue; // already closed
|
||||
const optimaField = raw.customFields?.find(
|
||||
(f: any) => f.id === OptimaType.FIELD_ID,
|
||||
);
|
||||
if (!optimaField?.value) continue;
|
||||
if (optimaField.value === OptimaType.ScheduleEntry) continue; // skip other schedule entries
|
||||
if (STAYS_OPEN_TYPES.has(optimaField.value as OptimaTypeValue)) {
|
||||
parentActivityCwId = raw.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal — schedule entry will be created without a parent
|
||||
console.warn(
|
||||
`[Workflow:ScheduleEntry] Could not resolve parent activity: ${err}`,
|
||||
);
|
||||
}
|
||||
|
||||
const activity = await ActivityController.create({
|
||||
name: `[Schedule Entry] ${payload.activityTypeValue} — ${opportunity.name}`,
|
||||
type: { id: 3 }, // HistoricEntry
|
||||
opportunity: { id: opportunity.cwOpportunityId },
|
||||
...(opportunity.companyCwId ? { company: { id: opportunity.companyCwId } } : {}),
|
||||
assignTo: { id: user.cwMemberId },
|
||||
notes: payload.note ?? "",
|
||||
...(dateStart ? { dateStart } : {}),
|
||||
...(dateEnd ? { dateEnd } : {}),
|
||||
});
|
||||
|
||||
// Build custom fields: always Optima_Type, plus Parent_Activity when resolved
|
||||
const customFields: any[] = [
|
||||
{
|
||||
id: OptimaType.FIELD_ID,
|
||||
caption: "Optima_Type",
|
||||
type: "Text",
|
||||
entryMethod: "List",
|
||||
numberOfDecimals: 0,
|
||||
value: OptimaType.ScheduleEntry,
|
||||
},
|
||||
];
|
||||
|
||||
if (parentActivityCwId != null) {
|
||||
customFields.push({
|
||||
id: PARENT_ACTIVITY_FIELD_ID,
|
||||
caption: "Parent_Activity",
|
||||
type: "Text",
|
||||
entryMethod: "EntryField",
|
||||
numberOfDecimals: 0,
|
||||
value: String(parentActivityCwId),
|
||||
});
|
||||
}
|
||||
|
||||
// Set Optima_Type (+ Parent_Activity) on the new schedule entry
|
||||
await activity.update([
|
||||
{
|
||||
op: "replace",
|
||||
path: "customFields",
|
||||
value: customFields,
|
||||
},
|
||||
]);
|
||||
|
||||
// Return a no-transition result (status unchanged)
|
||||
return {
|
||||
success: true,
|
||||
previousStatusId: currentStatus,
|
||||
newStatusId: currentStatus,
|
||||
previousStatus: StatusIdToKey[currentStatus] ?? null,
|
||||
newStatus: StatusIdToKey[currentStatus] ?? null,
|
||||
activitiesCreated: [activity],
|
||||
coldCheck: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Any cancelable status → Canceled
|
||||
*
|
||||
@@ -1732,6 +1957,9 @@ async function closeOpenWorkflowActivities(
|
||||
// Only close activities whose type is in the stays-open set
|
||||
if (!STAYS_OPEN_TYPES.has(optimaField.value as OptimaTypeValue)) continue;
|
||||
|
||||
// Never auto-close Schedule Entry activities — they close only when time is logged
|
||||
if (optimaField.value === OptimaType.ScheduleEntry) continue;
|
||||
|
||||
const closeDate = new Date().toISOString();
|
||||
const existingFields = (raw.customFields ?? []).map((f: any) =>
|
||||
f.id === CLOSE_DATE_FIELD_ID ? { ...f, value: closeDate } : f,
|
||||
@@ -1797,7 +2025,10 @@ export async function processOpportunityAction(
|
||||
}
|
||||
|
||||
// ── Close any open workflow activities from previous stage ──────────
|
||||
await closeOpenWorkflowActivities(opportunity.cwOpportunityId);
|
||||
// Skip for createScheduleEntry — we intentionally preserve open activities.
|
||||
if (action !== "createScheduleEntry") {
|
||||
await closeOpenWorkflowActivities(opportunity.cwOpportunityId);
|
||||
}
|
||||
|
||||
// ── Route to transition function ────────────────────────────────────
|
||||
let result: WorkflowResult;
|
||||
@@ -1856,6 +2087,16 @@ export async function processOpportunityAction(
|
||||
result = await reopenCancelledOpportunity(opportunity, user, payload);
|
||||
break;
|
||||
|
||||
case "sendBackForRevision":
|
||||
result = await sendBackForRevision(opportunity, user, payload);
|
||||
break;
|
||||
|
||||
case "createScheduleEntry":
|
||||
// Schedule Entry does not close open activities — skip that step.
|
||||
// We call it directly rather than falling through closeOpenWorkflowActivities.
|
||||
result = await createScheduleEntry(opportunity, user, payload);
|
||||
break;
|
||||
|
||||
default: {
|
||||
const _exhaustive: never = action;
|
||||
return fail(`Unknown workflow action: "${_exhaustive}"`);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
const connectWiseApi = axios.create({
|
||||
baseURL: `https://ttscw.totaltech.net/v4_6_release/apis/3.0/`,
|
||||
headers: {
|
||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
async function main() {
|
||||
// Fetch inactive catalog items
|
||||
const pageSize = 1000;
|
||||
let page = 1;
|
||||
const inactiveItems: any[] = [];
|
||||
|
||||
while (true) {
|
||||
const response = await connectWiseApi.get(
|
||||
`/procurement/catalog?page=${page}&pageSize=${pageSize}&conditions=inactiveFlag=true&fields=id,identifier,description,_info`,
|
||||
);
|
||||
if (response.data.length === 0) break;
|
||||
inactiveItems.push(...response.data);
|
||||
page++;
|
||||
}
|
||||
|
||||
console.log(`Found ${inactiveItems.length} inactive catalog items`);
|
||||
console.log(`Checking inventory for each (batches of 50)...\n`);
|
||||
|
||||
const withStock: any[] = [];
|
||||
const batchSize = 50;
|
||||
|
||||
for (let i = 0; i < inactiveItems.length; i += batchSize) {
|
||||
const batch = inactiveItems.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map(async (item) => {
|
||||
try {
|
||||
const res = await connectWiseApi.get(
|
||||
`/procurement/catalog/${item.id}/inventory?fields=onHand`,
|
||||
);
|
||||
const totalOnHand = (res.data as { onHand: number }[]).reduce(
|
||||
(sum, e) => sum + (e.onHand || 0),
|
||||
0,
|
||||
);
|
||||
if (totalOnHand > 0) {
|
||||
withStock.push({
|
||||
id: item.id,
|
||||
identifier: item.identifier,
|
||||
description: item.description,
|
||||
totalOnHand,
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}),
|
||||
);
|
||||
const done = Math.min(i + batchSize, inactiveItems.length);
|
||||
if (done % 500 === 0 || done === inactiveItems.length) {
|
||||
console.log(` ${done}/${inactiveItems.length} checked`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\nInactive items with inventory: ${withStock.length}/${inactiveItems.length}\n`,
|
||||
);
|
||||
|
||||
if (withStock.length > 0) {
|
||||
console.log(JSON.stringify(withStock, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,600 +0,0 @@
|
||||
/**
|
||||
* Test Script: CW Forecast Item Edit & Partial Cancellation
|
||||
*
|
||||
* This script performs read-write operations against the ConnectWise API:
|
||||
*
|
||||
* 1. Search all open opportunities for a forecast item with description
|
||||
* matching "labor Special Order" (case-insensitive).
|
||||
* 2. Report the current state of that item (price, cost, qty, etc.).
|
||||
* 3. PATCH the item: revenue → 72,000 | cost → 8,500 | quantity → 67
|
||||
* 4. Verify the update by re-fetching the forecast.
|
||||
* 5. Cancel 13 units via the linked procurement product
|
||||
* (partial cancellation: quantityCancelled = 13).
|
||||
* 6. Verify the cancellation by re-fetching procurement data.
|
||||
* 7. Report on every step.
|
||||
*
|
||||
* Usage: bun run test-cw-edit-item.ts
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
const cw = axios.create({
|
||||
baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
|
||||
headers: {
|
||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const log = (label: string, ...args: unknown[]) =>
|
||||
console.log(`\n[${label}]`, ...args);
|
||||
|
||||
const divider = () => console.log("─".repeat(72));
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const fmt = (n: number) =>
|
||||
n.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
// ── Types (minimal, for this script) ──────────────────────────────────────────
|
||||
|
||||
interface ForecastItem {
|
||||
id: number;
|
||||
forecastDescription: string;
|
||||
productDescription: string;
|
||||
quantity: number;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
margin: number;
|
||||
forecastType: string;
|
||||
sequenceNumber: number;
|
||||
catalogItem?: { id: number; identifier: string };
|
||||
status?: { id: number; name: string };
|
||||
opportunity?: { id: number; name: string };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Forecast {
|
||||
id: number;
|
||||
forecastItems: ForecastItem[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ProcurementProduct {
|
||||
id: number;
|
||||
forecastDetailId: number;
|
||||
description: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
cost: number;
|
||||
cancelledFlag: boolean;
|
||||
quantityCancelled: number;
|
||||
cancelledReason: string | null;
|
||||
cancelledBy: string | null;
|
||||
cancelledDate: string | null;
|
||||
opportunity?: { id: number };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
divider();
|
||||
log("START", "CW Forecast Item Edit & Cancellation Test");
|
||||
log("START", `Timestamp: ${new Date().toISOString()}`);
|
||||
divider();
|
||||
|
||||
// ── Step 1: Find the "labor Special Order" forecast item ────────────────
|
||||
|
||||
const OPP_ID = 5150;
|
||||
log(
|
||||
"SEARCH",
|
||||
`Looking for forecast item matching "labor Special Order" on opportunity ${OPP_ID}...`,
|
||||
);
|
||||
|
||||
// Fetch the forecast for opportunity 5150 directly
|
||||
let targetOppId: number = OPP_ID;
|
||||
let targetItem: ForecastItem | null = null;
|
||||
let targetForecast: Forecast | null = null;
|
||||
|
||||
const forecastRes = await cw.get(`/sales/opportunities/${OPP_ID}/forecast`);
|
||||
targetForecast = forecastRes.data as Forecast;
|
||||
const match = (targetForecast.forecastItems ?? []).find(
|
||||
(fi: ForecastItem) =>
|
||||
fi.forecastDescription?.toLowerCase().includes("special order") ||
|
||||
fi.productDescription?.toLowerCase().includes("special order"),
|
||||
);
|
||||
|
||||
if (match) {
|
||||
targetItem = match;
|
||||
log("SEARCH", `✓ FOUND forecast item on opportunity ${OPP_ID}`);
|
||||
}
|
||||
|
||||
if (!targetItem || !targetForecast) {
|
||||
log(
|
||||
"SEARCH",
|
||||
`✗ No "labor Special Order" item found on opportunity ${OPP_ID}.`,
|
||||
);
|
||||
log("SEARCH", "All forecast items on this opportunity:");
|
||||
for (const fi of targetForecast.forecastItems ?? []) {
|
||||
console.log(
|
||||
` id=${fi.id} "${fi.forecastDescription}" / "${fi.productDescription}"`,
|
||||
);
|
||||
}
|
||||
log("SEARCH", "Aborting.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Step 2: Report current state ────────────────────────────────────────
|
||||
|
||||
divider();
|
||||
log("CURRENT STATE", "Forecast item details BEFORE edit:");
|
||||
console.log(` Opportunity ID: ${targetOppId}`);
|
||||
console.log(` Forecast Item ID: ${targetItem.id}`);
|
||||
console.log(` Forecast Description: ${targetItem.forecastDescription}`);
|
||||
console.log(` Product Description: ${targetItem.productDescription}`);
|
||||
console.log(
|
||||
` Catalog Item: ${targetItem.catalogItem?.identifier ?? "(none)"} (cwId=${targetItem.catalogItem?.id ?? "N/A"})`,
|
||||
);
|
||||
console.log(` Forecast Type: ${targetItem.forecastType}`);
|
||||
console.log(
|
||||
` Status: ${targetItem.status?.name ?? "?"} (id=${targetItem.status?.id ?? "?"})`,
|
||||
);
|
||||
console.log(` Sequence Number: ${targetItem.sequenceNumber}`);
|
||||
console.log(` ──────────────────────────────────`);
|
||||
console.log(` Quantity: ${targetItem.quantity}`);
|
||||
console.log(` Revenue (Price): $${fmt(targetItem.revenue)}`);
|
||||
console.log(` Cost: $${fmt(targetItem.cost)}`);
|
||||
console.log(` Margin: $${fmt(targetItem.margin)}`);
|
||||
|
||||
// Also report all items on this opportunity for context
|
||||
const allItems = targetForecast.forecastItems ?? [];
|
||||
log(
|
||||
"CONTEXT",
|
||||
`Total forecast items on this opportunity: ${allItems.length}`,
|
||||
);
|
||||
for (const fi of allItems) {
|
||||
const marker = fi.id === targetItem.id ? " ◀ TARGET" : "";
|
||||
console.log(
|
||||
` [${fi.sequenceNumber}] id=${fi.id} "${fi.forecastDescription}" ` +
|
||||
`qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}${marker}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 3: PATCH the forecast item ─────────────────────────────────────
|
||||
|
||||
divider();
|
||||
const UNIT_PRICE = 72_000;
|
||||
const UNIT_COST = 8_500;
|
||||
const QTY = 67;
|
||||
const TOTAL_REVENUE = UNIT_PRICE * QTY; // $4,824,000
|
||||
const TOTAL_COST = UNIT_COST * QTY; // $569,500
|
||||
|
||||
log("EDIT", "Patching forecast item...");
|
||||
log(
|
||||
"EDIT",
|
||||
` Unit price: $${fmt(UNIT_PRICE)} × ${QTY} = $${fmt(TOTAL_REVENUE)} (revenue)`,
|
||||
);
|
||||
log(
|
||||
"EDIT",
|
||||
` Unit cost: $${fmt(UNIT_COST)} × ${QTY} = $${fmt(TOTAL_COST)} (cost)`,
|
||||
);
|
||||
log("EDIT", ` Quantity: ${QTY}`);
|
||||
|
||||
// Find the index of our target item in the forecast array
|
||||
const forecastItems = targetForecast.forecastItems ?? [];
|
||||
const targetIdx = forecastItems.findIndex((fi) => fi.id === targetItem!.id);
|
||||
|
||||
if (targetIdx === -1) {
|
||||
log(
|
||||
"EDIT",
|
||||
"✗ Could not find target item index in forecast array. Aborting.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log("EDIT", `Target item is at index ${targetIdx} in forecastItems array.`);
|
||||
|
||||
const patchOps = [
|
||||
{
|
||||
op: "replace",
|
||||
path: `/forecastItems/${targetIdx}/revenue`,
|
||||
value: TOTAL_REVENUE,
|
||||
},
|
||||
{
|
||||
op: "replace",
|
||||
path: `/forecastItems/${targetIdx}/cost`,
|
||||
value: TOTAL_COST,
|
||||
},
|
||||
{ op: "replace", path: `/forecastItems/${targetIdx}/quantity`, value: QTY },
|
||||
];
|
||||
|
||||
log("EDIT", "Patch operations:");
|
||||
for (const op of patchOps) {
|
||||
console.log(` ${op.op} ${op.path} → ${op.value}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const patchRes = await cw.patch(
|
||||
`/sales/opportunities/${targetOppId}/forecast`,
|
||||
patchOps,
|
||||
);
|
||||
const updatedForecast: Forecast = patchRes.data;
|
||||
const updatedItem = (updatedForecast.forecastItems ?? [])[targetIdx];
|
||||
|
||||
if (!updatedItem) {
|
||||
log("EDIT", "✗ Item not found at expected index after PATCH.");
|
||||
} else {
|
||||
log("EDIT", "✓ PATCH successful. Updated item:");
|
||||
console.log(` Forecast Item ID: ${updatedItem.id}`);
|
||||
console.log(` Forecast Description: ${updatedItem.forecastDescription}`);
|
||||
console.log(` Quantity: ${updatedItem.quantity}`);
|
||||
console.log(` Revenue (Price): $${fmt(updatedItem.revenue)}`);
|
||||
console.log(` Cost: $${fmt(updatedItem.cost)}`);
|
||||
console.log(` Margin: $${fmt(updatedItem.margin)}`);
|
||||
|
||||
// Verify values match what we set
|
||||
const checks = [
|
||||
{
|
||||
field: "revenue",
|
||||
expected: TOTAL_REVENUE,
|
||||
actual: updatedItem.revenue,
|
||||
},
|
||||
{ field: "cost", expected: TOTAL_COST, actual: updatedItem.cost },
|
||||
{ field: "quantity", expected: QTY, actual: updatedItem.quantity },
|
||||
];
|
||||
|
||||
log("VERIFY EDIT", "Checking values match requested:");
|
||||
for (const check of checks) {
|
||||
const ok = check.actual === check.expected;
|
||||
console.log(
|
||||
` ${ok ? "✓" : "✗"} ${check.field}: expected=${check.expected}, actual=${check.actual}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Update our reference for the cancellation step
|
||||
targetItem = updatedItem;
|
||||
}
|
||||
} catch (err: any) {
|
||||
log("EDIT", `✗ PATCH failed: ${err.response?.status ?? err.message}`);
|
||||
if (err.response?.data) {
|
||||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
||||
}
|
||||
|
||||
// If quantity PATCH failed (read-only), try without quantity
|
||||
if (err.response?.status === 400 || err.response?.status === 422) {
|
||||
log(
|
||||
"EDIT",
|
||||
"Retrying without quantity (may be read-only on forecast items)...",
|
||||
);
|
||||
const retryOps = patchOps.filter((op) => !op.path.endsWith("/quantity"));
|
||||
try {
|
||||
const retryRes = await cw.patch(
|
||||
`/sales/opportunities/${targetOppId}/forecast`,
|
||||
retryOps,
|
||||
);
|
||||
const retryForecast: Forecast = retryRes.data;
|
||||
const retryItem = (retryForecast.forecastItems ?? [])[targetIdx];
|
||||
|
||||
if (retryItem) {
|
||||
log(
|
||||
"EDIT",
|
||||
"✓ Retry PATCH successful (without quantity). Updated item:",
|
||||
);
|
||||
console.log(
|
||||
` Quantity: ${retryItem.quantity} (unchanged — read-only)`,
|
||||
);
|
||||
console.log(` Revenue (Price): $${fmt(retryItem.revenue)}`);
|
||||
console.log(` Cost: $${fmt(retryItem.cost)}`);
|
||||
console.log(` Margin: $${fmt(retryItem.margin)}`);
|
||||
targetItem = retryItem;
|
||||
}
|
||||
} catch (retryErr: any) {
|
||||
log(
|
||||
"EDIT",
|
||||
`✗ Retry also failed: ${retryErr.response?.status ?? retryErr.message}`,
|
||||
);
|
||||
if (retryErr.response?.data) {
|
||||
console.log(
|
||||
" Response:",
|
||||
JSON.stringify(retryErr.response.data, null, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 4: Re-fetch and confirm final forecast state ───────────────────
|
||||
|
||||
divider();
|
||||
log("RE-FETCH", "Fetching forecast to confirm final state...");
|
||||
await sleep(500);
|
||||
|
||||
const confirmRes = await cw.get(
|
||||
`/sales/opportunities/${targetOppId}/forecast`,
|
||||
);
|
||||
const confirmedForecast: Forecast = confirmRes.data;
|
||||
const confirmedItem = (confirmedForecast.forecastItems ?? []).find(
|
||||
(fi) => fi.id === targetItem!.id,
|
||||
);
|
||||
|
||||
if (confirmedItem) {
|
||||
log("CONFIRMED STATE", "Forecast item after edit:");
|
||||
console.log(` Forecast Item ID: ${confirmedItem.id}`);
|
||||
console.log(` Forecast Description: ${confirmedItem.forecastDescription}`);
|
||||
console.log(` Quantity: ${confirmedItem.quantity}`);
|
||||
console.log(` Revenue (Price): $${fmt(confirmedItem.revenue)}`);
|
||||
console.log(` Cost: $${fmt(confirmedItem.cost)}`);
|
||||
console.log(` Margin: $${fmt(confirmedItem.margin)}`);
|
||||
} else {
|
||||
log(
|
||||
"CONFIRMED STATE",
|
||||
"⚠ Could not find item by original ID — it may have been regenerated.",
|
||||
);
|
||||
log("CONFIRMED STATE", "All current forecast items:");
|
||||
for (const fi of confirmedForecast.forecastItems ?? []) {
|
||||
console.log(
|
||||
` id=${fi.id} "${fi.forecastDescription}" qty=${fi.quantity} rev=$${fmt(fi.revenue)} cost=$${fmt(fi.cost)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 5: Cancel 13 items via procurement product ─────────────────────
|
||||
|
||||
divider();
|
||||
log("CANCEL", "Cancelling 13 units on this item via procurement product...");
|
||||
|
||||
// First, find existing procurement products linked to this opportunity
|
||||
const procRes = await cw.get(
|
||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
|
||||
);
|
||||
const procProducts: ProcurementProduct[] = procRes.data;
|
||||
|
||||
log(
|
||||
"CANCEL",
|
||||
`Found ${procProducts.length} procurement product(s) on this opportunity.`,
|
||||
);
|
||||
|
||||
if (procProducts.length > 0) {
|
||||
for (const pp of procProducts) {
|
||||
console.log(
|
||||
` Proc id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
|
||||
`"${pp.description}" qty=${pp.quantity} price=$${fmt(pp.price ?? 0)} ` +
|
||||
`cancelled=${pp.cancelledFlag} qtyCancelled=${pp.quantityCancelled}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the procurement product linked to our forecast item
|
||||
const linkedProc = procProducts.find(
|
||||
(pp) => pp.forecastDetailId === targetItem!.id,
|
||||
);
|
||||
|
||||
if (linkedProc) {
|
||||
log("CANCEL", `Found linked procurement product: id=${linkedProc.id}`);
|
||||
log(
|
||||
"CANCEL",
|
||||
`Current state: cancelled=${linkedProc.cancelledFlag}, quantityCancelled=${linkedProc.quantityCancelled}`,
|
||||
);
|
||||
log("CANCEL", "Patching: quantityCancelled → 13, cancelledFlag → true");
|
||||
|
||||
try {
|
||||
const cancelRes = await cw.patch(
|
||||
`/procurement/products/${linkedProc.id}`,
|
||||
[
|
||||
{ op: "replace", path: "cancelledFlag", value: true },
|
||||
{ op: "replace", path: "quantityCancelled", value: 13 },
|
||||
{
|
||||
op: "replace",
|
||||
path: "cancelledReason",
|
||||
value: "Test cancellation — 13 units",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
log("CANCEL", "✓ Cancellation PATCH successful.");
|
||||
console.log(` cancelledFlag: ${cancelRes.data.cancelledFlag}`);
|
||||
console.log(` quantityCancelled: ${cancelRes.data.quantityCancelled}`);
|
||||
console.log(` cancelledReason: ${cancelRes.data.cancelledReason}`);
|
||||
console.log(
|
||||
` cancelledBy: ${cancelRes.data.cancelledBy ?? "N/A"}`,
|
||||
);
|
||||
console.log(
|
||||
` cancelledDate: ${cancelRes.data.cancelledDate ?? "N/A"}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
log(
|
||||
"CANCEL",
|
||||
`✗ Cancellation PATCH failed: ${err.response?.status ?? err.message}`,
|
||||
);
|
||||
if (err.response?.data) {
|
||||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log(
|
||||
"CANCEL",
|
||||
`No procurement product linked to forecast item id=${targetItem!.id}.`,
|
||||
);
|
||||
log(
|
||||
"CANCEL",
|
||||
"Creating a procurement product first, then cancelling 13...",
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a procurement product linked to this forecast item
|
||||
const createProcRes = await cw.post("/procurement/products", {
|
||||
catalogItem: targetItem!.catalogItem?.id
|
||||
? { id: targetItem!.catalogItem.id }
|
||||
: undefined,
|
||||
description:
|
||||
targetItem!.forecastDescription || targetItem!.productDescription,
|
||||
quantity: targetItem!.quantity || 67,
|
||||
price: targetItem!.revenue || 72_000,
|
||||
cost: targetItem!.cost || 8_500,
|
||||
billableOption: "Billable",
|
||||
opportunity: { id: targetOppId },
|
||||
forecastDetailId: targetItem!.id,
|
||||
});
|
||||
|
||||
const newProc = createProcRes.data;
|
||||
log("CANCEL", `✓ Created procurement product id=${newProc.id}`);
|
||||
console.log(` forecastDetailId: ${newProc.forecastDetailId}`);
|
||||
console.log(` description: ${newProc.description}`);
|
||||
console.log(` quantity: ${newProc.quantity}`);
|
||||
console.log(` price: $${fmt(newProc.price ?? 0)}`);
|
||||
console.log(` cost: $${fmt(newProc.cost ?? 0)}`);
|
||||
|
||||
// Now cancel 13 units
|
||||
log("CANCEL", "Patching procurement product: quantityCancelled → 13...");
|
||||
const cancelRes = await cw.patch(`/procurement/products/${newProc.id}`, [
|
||||
{ op: "replace", path: "cancelledFlag", value: true },
|
||||
{ op: "replace", path: "quantityCancelled", value: 13 },
|
||||
{
|
||||
op: "replace",
|
||||
path: "cancelledReason",
|
||||
value: "Test cancellation — 13 units",
|
||||
},
|
||||
]);
|
||||
|
||||
log("CANCEL", "✓ Cancellation PATCH successful.");
|
||||
console.log(` cancelledFlag: ${cancelRes.data.cancelledFlag}`);
|
||||
console.log(` quantityCancelled: ${cancelRes.data.quantityCancelled}`);
|
||||
console.log(` cancelledReason: ${cancelRes.data.cancelledReason}`);
|
||||
console.log(
|
||||
` cancelledBy: ${cancelRes.data.cancelledBy ?? "N/A"}`,
|
||||
);
|
||||
console.log(
|
||||
` cancelledDate: ${cancelRes.data.cancelledDate ?? "N/A"}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
log("CANCEL", `✗ Failed: ${err.response?.status ?? err.message}`);
|
||||
if (err.response?.data) {
|
||||
console.log(" Response:", JSON.stringify(err.response.data, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 6: Final verification ──────────────────────────────────────────
|
||||
|
||||
divider();
|
||||
log("FINAL VERIFY", "Re-fetching all data for final report...");
|
||||
await sleep(500);
|
||||
|
||||
// Re-fetch forecast
|
||||
const finalForecastRes = await cw.get(
|
||||
`/sales/opportunities/${targetOppId}/forecast`,
|
||||
);
|
||||
const finalForecast: Forecast = finalForecastRes.data;
|
||||
const finalItem =
|
||||
(finalForecast.forecastItems ?? []).find(
|
||||
(fi) => fi.id === targetItem!.id,
|
||||
) ??
|
||||
(finalForecast.forecastItems ?? []).find(
|
||||
(fi) =>
|
||||
fi.forecastDescription?.toLowerCase().includes("special order") ||
|
||||
fi.productDescription?.toLowerCase().includes("special order"),
|
||||
);
|
||||
|
||||
// Re-fetch procurement
|
||||
const finalProcRes = await cw.get(
|
||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${targetOppId}`)}&pageSize=1000`,
|
||||
);
|
||||
const finalProcs: ProcurementProduct[] = finalProcRes.data;
|
||||
|
||||
log("FINAL STATE — FORECAST ITEM", "");
|
||||
if (finalItem) {
|
||||
console.log(` Forecast Item ID: ${finalItem.id}`);
|
||||
console.log(` Forecast Description: ${finalItem.forecastDescription}`);
|
||||
console.log(` Quantity: ${finalItem.quantity}`);
|
||||
console.log(` Revenue (Price): $${fmt(finalItem.revenue)}`);
|
||||
console.log(` Cost: $${fmt(finalItem.cost)}`);
|
||||
console.log(` Margin: $${fmt(finalItem.margin)}`);
|
||||
} else {
|
||||
console.log(" ⚠ Target item not found in final forecast.");
|
||||
}
|
||||
|
||||
log("FINAL STATE — PROCUREMENT", `${finalProcs.length} product(s):`);
|
||||
for (const pp of finalProcs) {
|
||||
console.log(
|
||||
` id=${pp.id} forecastDetailId=${pp.forecastDetailId} ` +
|
||||
`"${pp.description}" qty=${pp.quantity} cancelled=${pp.cancelledFlag} ` +
|
||||
`qtyCancelled=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Summary ─────────────────────────────────────────────────────────────
|
||||
|
||||
divider();
|
||||
log("SUMMARY", "");
|
||||
|
||||
// After cancelling 13 of 67, CW recalculates totals for remaining 54 units
|
||||
const expectedFinalRevenue = Math.round(UNIT_PRICE * (QTY - 13) * 100) / 100;
|
||||
const expectedFinalCost = Math.round(UNIT_COST * (QTY - 13) * 100) / 100;
|
||||
|
||||
const editOk = finalItem
|
||||
? Math.abs(finalItem.revenue - expectedFinalRevenue) < 1 &&
|
||||
Math.abs(finalItem.cost - expectedFinalCost) < 1
|
||||
: false;
|
||||
const qtyOk = finalItem ? finalItem.quantity === QTY : false;
|
||||
|
||||
if (finalItem) {
|
||||
console.log(
|
||||
` Expected final revenue ($${fmt(UNIT_PRICE)} × ${QTY - 13}): $${fmt(expectedFinalRevenue)}`,
|
||||
);
|
||||
console.log(
|
||||
` Actual final revenue: $${fmt(finalItem.revenue)}`,
|
||||
);
|
||||
console.log(
|
||||
` Expected final cost ($${fmt(UNIT_COST)} × ${QTY - 13}): $${fmt(expectedFinalCost)}`,
|
||||
);
|
||||
console.log(
|
||||
` Actual final cost: $${fmt(finalItem.cost)}`,
|
||||
);
|
||||
}
|
||||
const cancelOk = finalProcs.some(
|
||||
(pp) =>
|
||||
pp.forecastDetailId === targetItem!.id &&
|
||||
pp.cancelledFlag === true &&
|
||||
pp.quantityCancelled === 13,
|
||||
);
|
||||
|
||||
console.log(
|
||||
` Unit price $${fmt(UNIT_PRICE)}/ea: `,
|
||||
editOk ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
console.log(
|
||||
` Unit cost $${fmt(UNIT_COST)}/ea: `,
|
||||
editOk ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
console.log(
|
||||
` Quantity set to ${QTY}: `,
|
||||
qtyOk ? "✓ PASS" : "✗ FAIL (may be read-only)",
|
||||
);
|
||||
console.log(
|
||||
" 13 units cancelled: ",
|
||||
cancelOk ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
|
||||
const allPass = editOk && qtyOk && cancelOk;
|
||||
divider();
|
||||
log(
|
||||
"RESULT",
|
||||
allPass
|
||||
? "✓ ALL CHECKS PASSED"
|
||||
: "⚠ SOME CHECKS DID NOT PASS — review output above",
|
||||
);
|
||||
divider();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("\n[FATAL]", err.response?.data ?? err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,442 +0,0 @@
|
||||
/**
|
||||
* Test Script: Forecast Item Resequencing & Procurement Linkage
|
||||
*
|
||||
* Validates the CW forecast API behaviour discovered via probing:
|
||||
* - `sequenceNumber` is read-only — display order = array position
|
||||
* - PUT always regenerates all forecast item IDs
|
||||
* - Revenue & cost are preserved through PUT
|
||||
* - PATCH on /forecast with `/forecastItems/{idx}/field` paths works
|
||||
* for some fields (e.g. forecastDescription) and preserves IDs
|
||||
*
|
||||
* Test flow:
|
||||
* 1. Create opportunity under XYZ Test Company
|
||||
* 2. Add 4 products via POST
|
||||
* 3. Create procurement products (linked by forecastDetailId)
|
||||
* 4. Cancel one procurement product
|
||||
* 5. Reorder forecast items via PUT (reverse order)
|
||||
* 6. Remap procurement forecastDetailId to new IDs
|
||||
* 7. Verify: order correct, prices preserved, cancellation data intact
|
||||
* 8. Clean up
|
||||
*
|
||||
* Usage: bun run test-forecast-resequence.ts
|
||||
*/
|
||||
import axios from "axios";
|
||||
|
||||
const cw = axios.create({
|
||||
baseURL: "https://ttscw.totaltech.net/v4_6_release/apis/3.0/",
|
||||
headers: {
|
||||
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
|
||||
clientId: `${process.env.CW_CLIENT_ID}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const log = (label: string, ...args: unknown[]) =>
|
||||
console.log(`\n[${label}]`, ...args);
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function main() {
|
||||
// ── 1. Find company ─────────────────────────────────────────────────────
|
||||
log("SETUP", "Finding XYZ Test Company...");
|
||||
const compRes = await cw.get(
|
||||
`/company/companies?conditions=${encodeURIComponent("name like 'XYZ Test%'")}&fields=id,identifier,name`,
|
||||
);
|
||||
if (compRes.data.length === 0) {
|
||||
console.error("ERROR: 'XYZ Test Company' not found.");
|
||||
process.exit(1);
|
||||
}
|
||||
const company = compRes.data[0];
|
||||
log("SETUP", `Company: ${company.name} (id=${company.id})`);
|
||||
|
||||
// ── 2. Create opportunity ───────────────────────────────────────────────
|
||||
log("SETUP", "Creating test opportunity...");
|
||||
const oppRes = await cw.post("/sales/opportunities", {
|
||||
name: `[TEST] Resequence – ${new Date().toISOString().slice(0, 16)}`,
|
||||
company: { id: company.id },
|
||||
contact: { id: 1 },
|
||||
primarySalesRep: { id: 153 },
|
||||
expectedCloseDate: new Date(Date.now() + 30 * 86_400_000)
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, "Z"),
|
||||
});
|
||||
const oppId = oppRes.data.id;
|
||||
log("SETUP", `Created opportunity id=${oppId}`);
|
||||
|
||||
const forecastUrl = `/sales/opportunities/${oppId}/forecast`;
|
||||
|
||||
// Track IDs for cleanup
|
||||
const procIdsToClean: number[] = [];
|
||||
|
||||
try {
|
||||
// ── 3. Add 4 products ───────────────────────────────────────────────────
|
||||
log("PRODUCTS", "Adding 4 products...");
|
||||
const postRes = await cw.post(forecastUrl, {
|
||||
forecastItems: [
|
||||
{
|
||||
opportunity: { id: oppId },
|
||||
status: { id: 1 },
|
||||
forecastDescription: "Alpha",
|
||||
revenue: 100,
|
||||
cost: 50,
|
||||
forecastType: "Product",
|
||||
},
|
||||
{
|
||||
opportunity: { id: oppId },
|
||||
status: { id: 1 },
|
||||
forecastDescription: "Bravo",
|
||||
revenue: 250,
|
||||
cost: 125,
|
||||
forecastType: "Product",
|
||||
},
|
||||
{
|
||||
opportunity: { id: oppId },
|
||||
status: { id: 1 },
|
||||
forecastDescription: "Charlie",
|
||||
revenue: 30,
|
||||
cost: 10,
|
||||
forecastType: "Product",
|
||||
},
|
||||
{
|
||||
opportunity: { id: oppId },
|
||||
status: { id: 1 },
|
||||
forecastDescription: "Delta",
|
||||
revenue: 75,
|
||||
cost: 40,
|
||||
forecastType: "Product",
|
||||
},
|
||||
],
|
||||
});
|
||||
const items: any[] = postRes.data.forecastItems ?? [];
|
||||
log("PRODUCTS", `Created ${items.length} items:`);
|
||||
for (const it of items) {
|
||||
console.log(
|
||||
` id=${it.id} desc="${it.forecastDescription}" rev=${it.revenue} cost=${it.cost}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Snapshot prices
|
||||
const priceSnap = new Map<string, { rev: number; cost: number }>(
|
||||
items.map((i) => [
|
||||
i.forecastDescription,
|
||||
{ rev: i.revenue, cost: i.cost },
|
||||
]),
|
||||
);
|
||||
|
||||
// ── 4. Create procurement products ──────────────────────────────────────
|
||||
log("PROCUREMENT", "Creating procurement products...");
|
||||
const procProducts: any[] = [];
|
||||
for (const item of items) {
|
||||
try {
|
||||
const pr = await cw.post("/procurement/products", {
|
||||
catalogItem: { id: 87 },
|
||||
description: item.forecastDescription,
|
||||
quantity: 1,
|
||||
price: item.revenue,
|
||||
cost: item.cost,
|
||||
billableOption: "Billable",
|
||||
opportunity: { id: oppId },
|
||||
forecastDetailId: item.id,
|
||||
});
|
||||
procProducts.push(pr.data);
|
||||
procIdsToClean.push(pr.data.id);
|
||||
console.log(
|
||||
` ✓ Proc ${pr.data.id} → forecastDetailId=${pr.data.forecastDetailId} "${item.forecastDescription}"`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.log(
|
||||
` ✗ Failed: ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (procProducts.length === 0) {
|
||||
log(
|
||||
"PROCUREMENT",
|
||||
"Could not create procurement products (permission issue?).",
|
||||
);
|
||||
log(
|
||||
"PROCUREMENT",
|
||||
"Will run reorder test without cancellation verification.",
|
||||
);
|
||||
}
|
||||
|
||||
// ── 5. Cancel "Bravo" procurement product ───────────────────────────────
|
||||
const bravoProc = procProducts.find((p: any) => p.description === "Bravo");
|
||||
if (bravoProc) {
|
||||
log("CANCEL", `Cancelling Bravo (proc id=${bravoProc.id})...`);
|
||||
try {
|
||||
await cw.patch(`/procurement/products/${bravoProc.id}`, [
|
||||
{ op: "replace", path: "cancelledFlag", value: true },
|
||||
{ op: "replace", path: "quantityCancelled", value: 1 },
|
||||
{
|
||||
op: "replace",
|
||||
path: "cancelledReason",
|
||||
value: "Test cancellation",
|
||||
},
|
||||
]);
|
||||
log("CANCEL", "✓ Cancelled.");
|
||||
} catch (e: any) {
|
||||
log(
|
||||
"CANCEL",
|
||||
`✗ ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5b. Check for auto-created forecast items ─────────────────────────
|
||||
await sleep(300);
|
||||
const midForecast = await cw.get(forecastUrl);
|
||||
const midItems = midForecast.data.forecastItems ?? [];
|
||||
log(
|
||||
"OBSERVE",
|
||||
`Forecast items after procurement creation: ${midItems.length} (was ${items.length})`,
|
||||
);
|
||||
if (midItems.length !== items.length) {
|
||||
log(
|
||||
"OBSERVE",
|
||||
"⚠ Creating procurement products auto-created additional forecast items!",
|
||||
);
|
||||
for (const mi of midItems) {
|
||||
const isOriginal = items.some((i: any) => i.id === mi.id);
|
||||
console.log(
|
||||
` id=${mi.id} desc="${mi.forecastDescription}" ${isOriginal ? "(original)" : "(AUTO-CREATED by procurement)"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot procurement state before reorder
|
||||
const beforeProc = await cw.get(
|
||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
|
||||
);
|
||||
// Build map by description for cross-PUT comparison (IDs will change)
|
||||
const beforeByDesc = new Map<string, any>();
|
||||
log(
|
||||
"SNAPSHOT",
|
||||
`${beforeProc.data.length} procurement products before reorder:`,
|
||||
);
|
||||
for (const p of beforeProc.data) {
|
||||
beforeByDesc.set(p.description, p);
|
||||
console.log(
|
||||
` Proc ${p.id}: forecastDetailId=${p.forecastDetailId} cancelled=${p.cancelledFlag} qty=${p.quantityCancelled} reason="${p.cancelledReason ?? ""}" "${p.description}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Record old procurement IDs for later comparison
|
||||
const oldProcIds = new Set(beforeProc.data.map((p: any) => p.id));
|
||||
|
||||
// ── 6. Reorder: reverse ONLY the original 4 forecast items ──────────────
|
||||
log("REORDER", "Reversing forecast item order via PUT...");
|
||||
|
||||
// Only reorder the original items; keep any auto-created ones in place
|
||||
const originalDescs = new Set(items.map((i: any) => i.forecastDescription));
|
||||
const originals = midItems.filter(
|
||||
(i: any) =>
|
||||
originalDescs.has(i.forecastDescription) &&
|
||||
items.some((o: any) => o.id === i.id),
|
||||
);
|
||||
const extras = midItems.filter(
|
||||
(i: any) => !originals.some((o: any) => o.id === i.id),
|
||||
);
|
||||
|
||||
const reversedOriginals = [...originals].reverse();
|
||||
const reorderedAll = [...reversedOriginals, ...extras];
|
||||
|
||||
const clone = JSON.parse(JSON.stringify(midForecast.data));
|
||||
clone.forecastItems = JSON.parse(JSON.stringify(reorderedAll));
|
||||
|
||||
const putRes = await cw.put(forecastUrl, clone);
|
||||
const newItems: any[] = putRes.data.forecastItems ?? [];
|
||||
|
||||
log("REORDER", `After PUT (${newItems.length} items):`);
|
||||
for (const it of newItems) {
|
||||
console.log(
|
||||
` id=${it.id} desc="${it.forecastDescription}" rev=${it.revenue} cost=${it.cost}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Build old→new ID map by position (for original items only)
|
||||
const idMap = new Map<number, number>();
|
||||
for (let i = 0; i < reversedOriginals.length && i < newItems.length; i++) {
|
||||
idMap.set(reversedOriginals[i].id, newItems[i].id);
|
||||
}
|
||||
log("ID MAP", "Forecast item Old → New:");
|
||||
for (const [oldId, newId] of idMap) {
|
||||
console.log(` ${oldId} → ${newId}`);
|
||||
}
|
||||
|
||||
// ── 7. Check if procurement products survived PUT ───────────────────────
|
||||
await sleep(300);
|
||||
const afterProc = await cw.get(
|
||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
|
||||
);
|
||||
const newProcIds = new Set(afterProc.data.map((p: any) => p.id));
|
||||
|
||||
log(
|
||||
"PROCUREMENT SURVIVAL",
|
||||
"Checking if procurement product IDs survived PUT...",
|
||||
);
|
||||
const procSurvived = [...oldProcIds].every((id) => newProcIds.has(id));
|
||||
if (procSurvived) {
|
||||
console.log(" ✓ All original procurement product IDs survived PUT.");
|
||||
} else {
|
||||
console.log(" ✗ PUT REGENERATED procurement product IDs!");
|
||||
console.log(` Before: [${[...oldProcIds].join(", ")}]`);
|
||||
console.log(` After: [${[...newProcIds].join(", ")}]`);
|
||||
}
|
||||
|
||||
// Try remap if old IDs still exist
|
||||
let remapOk = true;
|
||||
if (procSurvived) {
|
||||
log("REMAP", "Updating procurement products forecastDetailId...");
|
||||
for (const pp of beforeProc.data) {
|
||||
const oldFdId = pp.forecastDetailId as number;
|
||||
const newFdId = idMap.get(oldFdId);
|
||||
if (!newFdId || newFdId === oldFdId) continue;
|
||||
try {
|
||||
await cw.patch(`/procurement/products/${pp.id}`, [
|
||||
{ op: "replace", path: "forecastDetailId", value: newFdId },
|
||||
]);
|
||||
console.log(
|
||||
` ✓ Proc ${pp.id}: forecastDetailId ${oldFdId} → ${newFdId}`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
remapOk = false;
|
||||
console.log(
|
||||
` ✗ Proc ${pp.id} remap failed: ${e.response?.status} ${JSON.stringify(e.response?.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remapOk = false;
|
||||
log(
|
||||
"REMAP",
|
||||
"⚠ SKIPPED — procurement products were regenerated by PUT; old IDs no longer exist.",
|
||||
);
|
||||
}
|
||||
|
||||
// ── 8. Verify ───────────────────────────────────────────────────────────
|
||||
await sleep(300);
|
||||
|
||||
// 8a. Verify order (first 4 items)
|
||||
log("VERIFY ORDER", "Expected reverse: Delta, Charlie, Bravo, Alpha");
|
||||
const expectedOrder = ["Delta", "Charlie", "Bravo", "Alpha"];
|
||||
let orderOk = true;
|
||||
for (let i = 0; i < expectedOrder.length; i++) {
|
||||
const actual = newItems[i]?.forecastDescription;
|
||||
const ok = actual === expectedOrder[i];
|
||||
if (!ok) orderOk = false;
|
||||
console.log(
|
||||
` Position ${i}: ${ok ? "✓" : "✗"} expected "${expectedOrder[i]}", got "${actual}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// 8b. Verify prices (by description)
|
||||
log("VERIFY PRICES", "");
|
||||
let pricesOk = true;
|
||||
for (const item of newItems) {
|
||||
const orig = priceSnap.get(item.forecastDescription);
|
||||
if (!orig) continue;
|
||||
if (item.revenue !== orig.rev || item.cost !== orig.cost) {
|
||||
pricesOk = false;
|
||||
console.log(
|
||||
` ✗ "${item.forecastDescription}": rev ${orig.rev}→${item.revenue}, cost ${orig.cost}→${item.cost}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (pricesOk) console.log(" ✓ All prices preserved.");
|
||||
|
||||
// 8c. Verify cancellation data — match by description since IDs may have changed
|
||||
let cancelOk = true;
|
||||
if (procProducts.length > 0) {
|
||||
log(
|
||||
"VERIFY CANCELLATION",
|
||||
"Checking cancellation data on procurement products after PUT...",
|
||||
);
|
||||
const finalProc = await cw.get(
|
||||
`/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag,quantityCancelled,cancelledReason,description`,
|
||||
);
|
||||
|
||||
// Track by procIdsToClean for cleanup
|
||||
for (const p of finalProc.data) {
|
||||
if (!procIdsToClean.includes(p.id)) procIdsToClean.push(p.id);
|
||||
}
|
||||
|
||||
for (const pp of finalProc.data) {
|
||||
const orig = beforeByDesc.get(pp.description);
|
||||
if (!orig) {
|
||||
console.log(
|
||||
` ? Proc ${pp.id} "${pp.description}" — no matching pre-PUT record`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cancelledMatch =
|
||||
pp.cancelledFlag === orig.cancelledFlag &&
|
||||
pp.quantityCancelled === orig.quantityCancelled &&
|
||||
(pp.cancelledReason ?? "") === (orig.cancelledReason ?? "");
|
||||
|
||||
if (!cancelledMatch) {
|
||||
cancelOk = false;
|
||||
console.log(
|
||||
` ✗ Proc ${pp.id} "${pp.description}": CANCELLATION DATA CHANGED\n` +
|
||||
` Before: cancelled=${orig.cancelledFlag} qty=${orig.quantityCancelled} reason="${orig.cancelledReason ?? ""}"\n` +
|
||||
` After: cancelled=${pp.cancelledFlag} qty=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
` ✓ Proc ${pp.id} "${pp.description}": cancelled=${pp.cancelledFlag} qty=${pp.quantityCancelled} reason="${pp.cancelledReason ?? ""}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Summary ─────────────────────────────────────────────────────────────
|
||||
log("SUMMARY", "");
|
||||
console.log(
|
||||
" Order correct: ",
|
||||
orderOk ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
console.log(
|
||||
" Prices preserved: ",
|
||||
pricesOk ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
console.log(
|
||||
" Proc IDs survived PUT: ",
|
||||
procSurvived ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
console.log(
|
||||
" Procurement remap: ",
|
||||
remapOk ? "✓ PASS" : "✗ FAIL (skipped or failed)",
|
||||
);
|
||||
console.log(
|
||||
" Cancellation data preserved:",
|
||||
cancelOk ? "✓ PASS" : "✗ FAIL",
|
||||
);
|
||||
|
||||
const allPass = orderOk && pricesOk && procSurvived && remapOk && cancelOk;
|
||||
log("RESULT", allPass ? "✓ ALL TESTS PASSED" : "✗ SOME TESTS FAILED");
|
||||
} finally {
|
||||
// ── Cleanup ─────────────────────────────────────────────────────────────
|
||||
log("CLEANUP", "Deleting procurement products...");
|
||||
for (const id of procIdsToClean) {
|
||||
try {
|
||||
await cw.delete(`/procurement/products/${id}`);
|
||||
} catch {}
|
||||
}
|
||||
log("CLEANUP", `Deleted ${procIdsToClean.length} procurement products.`);
|
||||
|
||||
log("CLEANUP", `Deleting opportunity ${oppId}...`);
|
||||
try {
|
||||
await cw.delete(`/sales/opportunities/${oppId}`);
|
||||
log("CLEANUP", "✓ Done.");
|
||||
} catch (e: any) {
|
||||
log("CLEANUP", `✗ ${e.response?.status ?? e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("\n[FATAL]", err.response?.data ?? err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
// Test script to probe UniFi API endpoints for response shapes
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
const controllerBaseUrl = "https://unifi.totaltech.net";
|
||||
const site = "km9b1v8i";
|
||||
const username = "admin";
|
||||
const password = "Tt$Un1fiIZth3B3$t26";
|
||||
|
||||
class TestClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
validateStatus: (s) => s >= 200 && s < 400,
|
||||
});
|
||||
}
|
||||
|
||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
||||
const raw = res.headers["set-cookie"];
|
||||
if (raw) {
|
||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
||||
}
|
||||
const csrf = res.headers["x-csrf-token"];
|
||||
if (typeof csrf === "string") {
|
||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
||||
}
|
||||
}
|
||||
|
||||
async login(): Promise<void> {
|
||||
try {
|
||||
const res = await this.client.post("/api/auth/login", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
console.log("Login OK (UniFi OS)", res.status);
|
||||
this.persistSession(res);
|
||||
} catch (e) {
|
||||
const res = await this.client.post("/api/login", { username, password });
|
||||
console.log("Login OK (legacy)", res.status);
|
||||
this.persistSession(res);
|
||||
}
|
||||
}
|
||||
|
||||
async tryGet(label: string, paths: string[]): Promise<any> {
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const data = res.data?.data ?? res.data;
|
||||
console.log(`\n=== ${label} (${path}) ===`);
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
return data;
|
||||
} catch (e: any) {
|
||||
console.log(` Failed ${path}: ${e.response?.status ?? e.message}`);
|
||||
}
|
||||
}
|
||||
console.log(` Could not fetch ${label} from any path`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = new TestClient(controllerBaseUrl);
|
||||
await client.login();
|
||||
|
||||
// 1. WLAN Groups (AP groups in UniFi)
|
||||
await client.tryGet("WLAN Groups", [
|
||||
`/proxy/network/api/s/${site}/rest/wlangroup`,
|
||||
`/api/s/${site}/rest/wlangroup`,
|
||||
]);
|
||||
|
||||
// 2. User Groups (bandwidth/speed limit profiles)
|
||||
await client.tryGet("User Groups (Speed Profiles)", [
|
||||
`/proxy/network/api/s/${site}/rest/usergroup`,
|
||||
`/api/s/${site}/rest/usergroup`,
|
||||
]);
|
||||
|
||||
// 3. Devices - APs only (compact)
|
||||
const devices = await client.tryGet("Devices", [
|
||||
`/proxy/network/api/s/${site}/stat/device`,
|
||||
]);
|
||||
if (devices) {
|
||||
const aps = devices.filter((d: any) => d.type === "uap");
|
||||
console.log(`\n=== APs (${aps.length}) - compact ===`);
|
||||
aps.forEach((ap: any) => {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
_id: ap._id,
|
||||
name: ap.name,
|
||||
mac: ap.mac,
|
||||
model: ap.model,
|
||||
radio_table: ap.radio_table?.map((r: any) => ({
|
||||
radio: r.radio,
|
||||
name: r.name,
|
||||
})),
|
||||
wlangroup_id_ng: ap.wlangroup_id_ng,
|
||||
wlangroup_id_na: ap.wlangroup_id_na,
|
||||
vap_table_count: ap.vap_table?.length,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. One full WLAN to see private_preshared_keys structure
|
||||
const wlans = await client.tryGet("WLANs", [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
||||
]);
|
||||
if (wlans) {
|
||||
// Log just the PPSK-related fields from each WLAN
|
||||
console.log("\n=== PPSK fields per WLAN ===");
|
||||
wlans.forEach((w: any) => {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
name: w.name,
|
||||
_id: w._id,
|
||||
private_preshared_keys_enabled: w.private_preshared_keys_enabled,
|
||||
private_preshared_keys: w.private_preshared_keys,
|
||||
ap_group_ids: w.ap_group_ids,
|
||||
ap_group_mode: w.ap_group_mode,
|
||||
wlan_band: w.wlan_band,
|
||||
wlan_bands: w.wlan_bands,
|
||||
usergroup_id: w.usergroup_id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => console.error(e));
|
||||
@@ -1,123 +0,0 @@
|
||||
// unifi-wifi-list.ts
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
const controllerBaseUrl = "https://unifi.totaltech.net";
|
||||
const site = "km9b1v8i";
|
||||
const username = "admin";
|
||||
const password = "Tt$Un1fiIZth3B3$t26";
|
||||
|
||||
interface WlanConfRaw {
|
||||
_id: string;
|
||||
name?: string;
|
||||
ssid?: string;
|
||||
x_passphrase?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface WlanConf {
|
||||
id: string;
|
||||
ssid: string;
|
||||
password: string | null;
|
||||
}
|
||||
|
||||
class UnifiClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
validateStatus: (s) => s >= 200 && s < 400,
|
||||
});
|
||||
}
|
||||
|
||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
||||
// Cookies
|
||||
const raw = res.headers["set-cookie"];
|
||||
if (raw) {
|
||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
||||
}
|
||||
// CSRF token (UniFi OS)
|
||||
const csrf = res.headers["x-csrf-token"];
|
||||
if (typeof csrf === "string") {
|
||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<void> {
|
||||
const body = { username, password };
|
||||
|
||||
try {
|
||||
// UniFi OS
|
||||
const res = await this.client.post("/api/auth/login", body);
|
||||
console.log("Login OK (UniFi OS)", res.status);
|
||||
this.persistSession(res);
|
||||
} catch (e) {
|
||||
// Legacy controller
|
||||
console.log("UniFi OS login failed, trying legacy...");
|
||||
const res = await this.client.post("/api/login", body);
|
||||
console.log("Login OK (legacy)", res.status);
|
||||
this.persistSession(res);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWlanConfRaw(site: string): Promise<WlanConfRaw[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
||||
`/api/s/${site}/rest/wlanconf`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const data = (res.data?.data ?? res.data) as WlanConfRaw[];
|
||||
console.log(`Fetched wlan from ${path}`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Failed ${path}:`,
|
||||
axios.isAxiosError(e) ? e.response?.status : e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch WLAN config from any known path");
|
||||
}
|
||||
|
||||
async getWlanConf(site: string): Promise<WlanConf[]> {
|
||||
const raw = await this.fetchWlanConfRaw(site);
|
||||
|
||||
return raw.map(
|
||||
(w): WlanConf => ({
|
||||
id: w._id,
|
||||
ssid: (w.name || w.ssid || "").toString(),
|
||||
password: typeof w.x_passphrase === "string" ? w.x_passphrase : null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const unifi = new UnifiClient(controllerBaseUrl);
|
||||
|
||||
try {
|
||||
await unifi.login(username, password);
|
||||
|
||||
const wlans = await unifi.getWlanConf(site);
|
||||
|
||||
wlans.forEach((wlan) => {
|
||||
console.log(`${wlan.ssid}: ${wlan.password ?? "<no password>"}`);
|
||||
});
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error("HTTP error", err.response?.status, err.response?.data);
|
||||
} else {
|
||||
console.error("Error", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => console.error(e));
|
||||
@@ -1,123 +0,0 @@
|
||||
// unifi-wifi-list.ts
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
const controllerBaseUrl = "https://unifi.totaltech.net";
|
||||
const site = "km9b1v8i";
|
||||
const username = "admin";
|
||||
const password = "Tt$Un1fiIZth3B3$t26";
|
||||
|
||||
interface WlanConfRaw {
|
||||
_id: string;
|
||||
name?: string;
|
||||
ssid?: string;
|
||||
x_passphrase?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface WlanConf {
|
||||
id: string;
|
||||
ssid: string;
|
||||
password: string | null;
|
||||
}
|
||||
|
||||
class UnifiClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
validateStatus: (s) => s >= 200 && s < 400,
|
||||
});
|
||||
}
|
||||
|
||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
||||
// Cookies
|
||||
const raw = res.headers["set-cookie"];
|
||||
if (raw) {
|
||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
||||
}
|
||||
// CSRF token (UniFi OS)
|
||||
const csrf = res.headers["x-csrf-token"];
|
||||
if (typeof csrf === "string") {
|
||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<void> {
|
||||
const body = { username, password };
|
||||
|
||||
try {
|
||||
// UniFi OS
|
||||
const res = await this.client.post("/api/auth/login", body);
|
||||
console.log("Login OK (UniFi OS)", res.status);
|
||||
this.persistSession(res);
|
||||
} catch (e) {
|
||||
// Legacy controller
|
||||
console.log("UniFi OS login failed, trying legacy...");
|
||||
const res = await this.client.post("/api/login", body);
|
||||
console.log("Login OK (legacy)", res.status);
|
||||
this.persistSession(res);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWlanConfRaw(site: string): Promise<WlanConfRaw[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
||||
`/api/s/${site}/rest/wlanconf`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const data = (res.data?.data ?? res.data) as WlanConfRaw[];
|
||||
console.log(`Fetched wlan from ${path}`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Failed ${path}:`,
|
||||
axios.isAxiosError(e) ? e.response?.status : e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch WLAN config from any known path");
|
||||
}
|
||||
|
||||
async getWlanConf(site: string): Promise<WlanConf[]> {
|
||||
const raw = await this.fetchWlanConfRaw(site);
|
||||
|
||||
return raw.map(
|
||||
(w): WlanConf => ({
|
||||
id: w._id,
|
||||
ssid: (w.name || w.ssid || "").toString(),
|
||||
password: typeof w.x_passphrase === "string" ? w.x_passphrase : null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const unifi = new UnifiClient(controllerBaseUrl);
|
||||
|
||||
try {
|
||||
await unifi.login(username, password);
|
||||
|
||||
const wlans = await unifi.getWlanConf(site);
|
||||
|
||||
wlans.forEach((wlan) => {
|
||||
console.log(`${wlan.ssid}: ${wlan.password ?? "<no password>"}`);
|
||||
});
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
console.error("HTTP error", err.response?.status, err.response?.data);
|
||||
} else {
|
||||
console.error("Error", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => console.error(e));
|
||||
@@ -1,15 +0,0 @@
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "./generated/prisma/client";
|
||||
import fs from "node:fs";
|
||||
|
||||
const env = fs.readFileSync(".env", "utf8");
|
||||
const line = env.split(/\n/).find((l) => l.startsWith("DATABASE_URL="));
|
||||
const url = line?.slice("DATABASE_URL=".length)?.replace(/^"|"$/g, "");
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: url }),
|
||||
});
|
||||
|
||||
const rows = await prisma.$queryRaw`select tablename from pg_tables where schemaname = 'public' order by tablename`;
|
||||
console.log(rows);
|
||||
await prisma.$disconnect();
|
||||
@@ -1,24 +0,0 @@
|
||||
import { prisma } from "./src/constants";
|
||||
|
||||
const roles = await prisma.role.findMany({ select: { title: true, moniker: true, permissions: true } });
|
||||
|
||||
for (const role of roles) {
|
||||
const token = role.permissions;
|
||||
if (!token) { console.log(role.title, "- NO PERMISSIONS"); continue; }
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
||||
const perms: string[] = payload.permissions || [];
|
||||
const relevant = perms.filter((p: string) =>
|
||||
p.includes("sales.opportunity.product") ||
|
||||
p.includes("sales.opportunity.*") ||
|
||||
p.includes("sales.*") ||
|
||||
p === "*"
|
||||
);
|
||||
if (relevant.length > 0) {
|
||||
console.log("=== " + role.title + " (" + role.moniker + ") ===");
|
||||
relevant.forEach((p: string) => console.log(" " + p));
|
||||
}
|
||||
} catch(e: any) { console.log(role.title, "- parse error:", e.message); }
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
@@ -1,24 +0,0 @@
|
||||
import { prisma } from "./src/constants";
|
||||
|
||||
const roles = await prisma.role.findMany({ select: { title: true, moniker: true, permissions: true } });
|
||||
|
||||
for (const role of roles) {
|
||||
const token = role.permissions;
|
||||
if (!token) { console.log(role.title, "- NO PERMISSIONS"); continue; }
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
||||
const perms: string[] = payload.permissions || [];
|
||||
// Check for obj.catalogItem or wildcard that would cover it
|
||||
const relevant = perms.filter((p: string) =>
|
||||
p.includes("obj.catalogItem") ||
|
||||
p.includes("obj.*") ||
|
||||
p === "*"
|
||||
);
|
||||
console.log("=== " + role.title + " (" + role.moniker + ") ===");
|
||||
console.log(" obj.catalogItem-related perms:", JSON.stringify(relevant));
|
||||
// Also show all perms for debugging
|
||||
console.log(" Total perms:", perms.length);
|
||||
} catch(e: any) { console.log(role.title, "- parse error:", e.message); }
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
|
||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
||||
|
||||
Commands marked with * may be preceded by a number, _N.
|
||||
Notes in parentheses indicate the behavior if _N is given.
|
||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
||||
|
||||
h H Display this help.
|
||||
q :q Q :Q ZZ Exit.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMOOVVIINNGG
|
||||
|
||||
e ^E j ^N CR * Forward one line (or _N lines).
|
||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
||||
b ^B ESC-v * Backward one window (or _N lines).
|
||||
z * Forward one window (and set window to _N).
|
||||
w * Backward one window (and set window to _N).
|
||||
ESC-SPACE * Forward one window, but don't stop at end-of-file.
|
||||
d ^D * Forward one half-window (and set half-window to _N).
|
||||
u ^U * Backward one half-window (and set half-window to _N).
|
||||
ESC-) RightArrow * Right one half screen width (or _N positions).
|
||||
ESC-( LeftArrow * Left one half screen width (or _N positions).
|
||||
ESC-} ^RightArrow Right to last column displayed.
|
||||
ESC-{ ^LeftArrow Left to first column.
|
||||
F Forward forever; like "tail -f".
|
||||
ESC-F Like F but stop when search pattern is found.
|
||||
r ^R ^L Repaint screen.
|
||||
R Repaint screen, discarding buffered input.
|
||||
---------------------------------------------------
|
||||
Default "window" is the screen height.
|
||||
Default "half-window" is half of the screen height.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
SSEEAARRCCHHIINNGG
|
||||
|
||||
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
|
||||
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
|
||||
n * Repeat previous search (for _N-th occurrence).
|
||||
N * Repeat previous search in reverse direction.
|
||||
ESC-n * Repeat previous search, spanning files.
|
||||
ESC-N * Repeat previous search, reverse dir. & spanning files.
|
||||
ESC-u Undo (toggle) search highlighting.
|
||||
ESC-U Clear search highlighting.
|
||||
&_p_a_t_t_e_r_n * Display only matching lines.
|
||||
---------------------------------------------------
|
||||
A search pattern may begin with one or more of:
|
||||
^N or ! Search for NON-matching lines.
|
||||
^E or * Search multiple files (pass thru END OF FILE).
|
||||
^F or @ Start search at FIRST file (for /) or last file (for ?).
|
||||
^K Highlight matches, but don't move (KEEP position).
|
||||
^R Don't use REGULAR EXPRESSIONS.
|
||||
^W WRAP search if no match found.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
JJUUMMPPIINNGG
|
||||
|
||||
g < ESC-< * Go to first line in file (or line _N).
|
||||
G > ESC-> * Go to last line in file (or line _N).
|
||||
p % * Go to beginning of file (or _N percent into file).
|
||||
t * Go to the (_N-th) next tag.
|
||||
T * Go to the (_N-th) previous tag.
|
||||
{ ( [ * Find close bracket } ) ].
|
||||
} ) ] * Find open bracket { ( [.
|
||||
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
|
||||
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
|
||||
---------------------------------------------------
|
||||
Each "find close bracket" command goes forward to the close bracket
|
||||
matching the (_N-th) open bracket in the top line.
|
||||
Each "find open bracket" command goes backward to the open bracket
|
||||
matching the (_N-th) close bracket in the bottom line.
|
||||
|
||||
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
|
||||
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
|
||||
'_<_l_e_t_t_e_r_> Go to a previously marked position.
|
||||
'' Go to the previous position.
|
||||
^X^X Same as '.
|
||||
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
|
||||
---------------------------------------------------
|
||||
A mark is any upper-case or lower-case letter.
|
||||
Certain marks are predefined:
|
||||
^ means beginning of the file
|
||||
$ means end of the file
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CCHHAANNGGIINNGG FFIILLEESS
|
||||
|
||||
:e [_f_i_l_e] Examine a new file.
|
||||
^X^V Same as :e.
|
||||
:n * Examine the (_N-th) next file from the command line.
|
||||
:p * Examine the (_N-th) previous file from the command line.
|
||||
:x * Examine the first (or _N-th) file from the command line.
|
||||
:d Delete the current file from the command line list.
|
||||
= ^G :f Print current file name.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
|
||||
|
||||
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
|
||||
--_<_n_a_m_e_> Toggle a command line option, by name.
|
||||
__<_f_l_a_g_> Display the setting of a command line option.
|
||||
___<_n_a_m_e_> Display the setting of an option, by name.
|
||||
+_c_m_d Execute the less cmd each time a new file is examined.
|
||||
|
||||
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|
||||
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
|
||||
s _f_i_l_e Save input to a file.
|
||||
v Edit the current file with $VISUAL or $EDITOR.
|
||||
V Print version number of "less".
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
OOPPTTIIOONNSS
|
||||
|
||||
Most options may be changed either on the command line,
|
||||
or from within less by using the - or -- command.
|
||||
Options may be given in one of two forms: either a single
|
||||
character preceded by a -, or a name preceded by --.
|
||||
|
||||
-? ........ --help
|
||||
Display help (from command line).
|
||||
-a ........ --search-skip-screen
|
||||
Search skips current screen.
|
||||
-A ........ --SEARCH-SKIP-SCREEN
|
||||
Search starts just after target line.
|
||||
-b [_N] .... --buffers=[_N]
|
||||
Number of buffers.
|
||||
-B ........ --auto-buffers
|
||||
Don't automatically allocate buffers for pipes.
|
||||
-c ........ --clear-screen
|
||||
Repaint by clearing rather than scrolling.
|
||||
-d ........ --dumb
|
||||
Dumb terminal.
|
||||
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
|
||||
Set screen colors.
|
||||
-e -E .... --quit-at-eof --QUIT-AT-EOF
|
||||
Quit at end of file.
|
||||
-f ........ --force
|
||||
Force open non-regular files.
|
||||
-F ........ --quit-if-one-screen
|
||||
Quit if entire file fits on first screen.
|
||||
-g ........ --hilite-search
|
||||
Highlight only last match for searches.
|
||||
-G ........ --HILITE-SEARCH
|
||||
Don't highlight any matches for searches.
|
||||
-h [_N] .... --max-back-scroll=[_N]
|
||||
Backward scroll limit.
|
||||
-i ........ --ignore-case
|
||||
Ignore case in searches that do not contain uppercase.
|
||||
-I ........ --IGNORE-CASE
|
||||
Ignore case in all searches.
|
||||
-j [_N] .... --jump-target=[_N]
|
||||
Screen position of target lines.
|
||||
-J ........ --status-column
|
||||
Display a status column at left edge of screen.
|
||||
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
|
||||
Use a lesskey file.
|
||||
-K ........ --quit-on-intr
|
||||
Exit less in response to ctrl-C.
|
||||
-L ........ --no-lessopen
|
||||
Ignore the LESSOPEN environment variable.
|
||||
-m -M .... --long-prompt --LONG-PROMPT
|
||||
Set prompt style.
|
||||
-n -N .... --line-numbers --LINE-NUMBERS
|
||||
Don't use line numbers.
|
||||
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
|
||||
Copy to log file (standard input only).
|
||||
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
|
||||
Copy to log file (unconditionally overwrite).
|
||||
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
|
||||
Start at pattern (from command line).
|
||||
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
|
||||
Define new prompt.
|
||||
-q -Q .... --quiet --QUIET --silent --SILENT
|
||||
Quiet the terminal bell.
|
||||
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
|
||||
Output "raw" control characters.
|
||||
-s ........ --squeeze-blank-lines
|
||||
Squeeze multiple blank lines.
|
||||
-S ........ --chop-long-lines
|
||||
Chop (truncate) long lines rather than wrapping.
|
||||
-t [_t_a_g] .. --tag=[_t_a_g]
|
||||
Find a tag.
|
||||
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
|
||||
Use an alternate tags file.
|
||||
-u -U .... --underline-special --UNDERLINE-SPECIAL
|
||||
Change handling of backspaces.
|
||||
-V ........ --version
|
||||
Display the version number of "less".
|
||||
-w ........ --hilite-unread
|
||||
Highlight first new line after forward-screen.
|
||||
-W ........ --HILITE-UNREAD
|
||||
Highlight first new line after any forward movement.
|
||||
-x [_N[,...]] --tabs=[_N[,...]]
|
||||
Set tab stops.
|
||||
-X ........ --no-init
|
||||
Don't use termcap init/deinit strings.
|
||||
-y [_N] .... --max-forw-scroll=[_N]
|
||||
Forward scroll limit.
|
||||
-z [_N] .... --window=[_N]
|
||||
Set size of window.
|
||||
-" [_c[_c]] . --quotes=[_c[_c]]
|
||||
Set shell quote characters.
|
||||
-~ ........ --tilde
|
||||
Don't display tildes after end of file.
|
||||
-# [_N] .... --shift=[_N]
|
||||
Set horizontal scroll amount (0 = one half screen width).
|
||||
--file-size
|
||||
Automatically determine the size of the input file.
|
||||
--follow-name
|
||||
The F command changes files if the input file is renamed.
|
||||
--incsearch
|
||||
Search file as each pattern character is typed in.
|
||||
--line-num-width=N
|
||||
Set the width of the -N line number field to N characters.
|
||||
--mouse
|
||||
Enable mouse input.
|
||||
--no-keypad
|
||||
Don't send termcap keypad init/deinit strings.
|
||||
--no-histdups
|
||||
Remove duplicates from command history.
|
||||
--rscroll=C
|
||||
Set the character used to mark truncated lines.
|
||||
--save-marks
|
||||
Retain marks across invocations of less.
|
||||
--status-col-width=N
|
||||
Set the width of the -J status column to N characters.
|
||||
--use-backslash
|
||||
Subsequent options use backslash as escape char.
|
||||
--use-color
|
||||
Enables colored text.
|
||||
--wheel-lines=N
|
||||
Each click of the mouse wheel moves N lines.
|
||||
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
LLIINNEE EEDDIITTIINNGG
|
||||
|
||||
These keys can be used to edit text being entered
|
||||
on the "command line" at the bottom of the screen.
|
||||
|
||||
RightArrow ..................... ESC-l ... Move cursor right one character.
|
||||
LeftArrow ...................... ESC-h ... Move cursor left one character.
|
||||
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
|
||||
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
|
||||
HOME ........................... ESC-0 ... Move cursor to start of line.
|
||||
END ............................ ESC-$ ... Move cursor to end of line.
|
||||
BACKSPACE ................................ Delete char to left of cursor.
|
||||
DELETE ......................... ESC-x ... Delete char under cursor.
|
||||
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
|
||||
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
|
||||
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
|
||||
UpArrow ........................ ESC-k ... Retrieve previous command line.
|
||||
DownArrow ...................... ESC-j ... Retrieve next command line.
|
||||
TAB ...................................... Complete filename & cycle.
|
||||
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
|
||||
ctrl-L ................................... Complete filename, list all.
|
||||
@@ -0,0 +1,258 @@
|
||||
|
||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
||||
|
||||
Commands marked with * may be preceded by a number, _N.
|
||||
Notes in parentheses indicate the behavior if _N is given.
|
||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
||||
|
||||
h H Display this help.
|
||||
q :q Q :Q ZZ Exit.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMOOVVIINNGG
|
||||
|
||||
e ^E j ^N CR * Forward one line (or _N lines).
|
||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
||||
b ^B ESC-v * Backward one window (or _N lines).
|
||||
z * Forward one window (and set window to _N).
|
||||
w * Backward one window (and set window to _N).
|
||||
ESC-SPACE * Forward one window, but don't stop at end-of-file.
|
||||
d ^D * Forward one half-window (and set half-window to _N).
|
||||
u ^U * Backward one half-window (and set half-window to _N).
|
||||
ESC-) RightArrow * Right one half screen width (or _N positions).
|
||||
ESC-( LeftArrow * Left one half screen width (or _N positions).
|
||||
ESC-} ^RightArrow Right to last column displayed.
|
||||
ESC-{ ^LeftArrow Left to first column.
|
||||
F Forward forever; like "tail -f".
|
||||
ESC-F Like F but stop when search pattern is found.
|
||||
r ^R ^L Repaint screen.
|
||||
R Repaint screen, discarding buffered input.
|
||||
---------------------------------------------------
|
||||
Default "window" is the screen height.
|
||||
Default "half-window" is half of the screen height.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
SSEEAARRCCHHIINNGG
|
||||
|
||||
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
|
||||
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
|
||||
n * Repeat previous search (for _N-th occurrence).
|
||||
N * Repeat previous search in reverse direction.
|
||||
ESC-n * Repeat previous search, spanning files.
|
||||
ESC-N * Repeat previous search, reverse dir. & spanning files.
|
||||
ESC-u Undo (toggle) search highlighting.
|
||||
ESC-U Clear search highlighting.
|
||||
&_p_a_t_t_e_r_n * Display only matching lines.
|
||||
---------------------------------------------------
|
||||
A search pattern may begin with one or more of:
|
||||
^N or ! Search for NON-matching lines.
|
||||
^E or * Search multiple files (pass thru END OF FILE).
|
||||
^F or @ Start search at FIRST file (for /) or last file (for ?).
|
||||
^K Highlight matches, but don't move (KEEP position).
|
||||
^R Don't use REGULAR EXPRESSIONS.
|
||||
^W WRAP search if no match found.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
JJUUMMPPIINNGG
|
||||
|
||||
g < ESC-< * Go to first line in file (or line _N).
|
||||
G > ESC-> * Go to last line in file (or line _N).
|
||||
p % * Go to beginning of file (or _N percent into file).
|
||||
t * Go to the (_N-th) next tag.
|
||||
T * Go to the (_N-th) previous tag.
|
||||
{ ( [ * Find close bracket } ) ].
|
||||
} ) ] * Find open bracket { ( [.
|
||||
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
|
||||
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
|
||||
---------------------------------------------------
|
||||
Each "find close bracket" command goes forward to the close bracket
|
||||
matching the (_N-th) open bracket in the top line.
|
||||
Each "find open bracket" command goes backward to the open bracket
|
||||
matching the (_N-th) close bracket in the bottom line.
|
||||
|
||||
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
|
||||
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
|
||||
'_<_l_e_t_t_e_r_> Go to a previously marked position.
|
||||
'' Go to the previous position.
|
||||
^X^X Same as '.
|
||||
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
|
||||
---------------------------------------------------
|
||||
A mark is any upper-case or lower-case letter.
|
||||
Certain marks are predefined:
|
||||
^ means beginning of the file
|
||||
$ means end of the file
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CCHHAANNGGIINNGG FFIILLEESS
|
||||
|
||||
:e [_f_i_l_e] Examine a new file.
|
||||
^X^V Same as :e.
|
||||
:n * Examine the (_N-th) next file from the command line.
|
||||
:p * Examine the (_N-th) previous file from the command line.
|
||||
:x * Examine the first (or _N-th) file from the command line.
|
||||
:d Delete the current file from the command line list.
|
||||
= ^G :f Print current file name.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
|
||||
|
||||
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
|
||||
--_<_n_a_m_e_> Toggle a command line option, by name.
|
||||
__<_f_l_a_g_> Display the setting of a command line option.
|
||||
___<_n_a_m_e_> Display the setting of an option, by name.
|
||||
+_c_m_d Execute the less cmd each time a new file is examined.
|
||||
|
||||
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|
||||
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
|
||||
s _f_i_l_e Save input to a file.
|
||||
v Edit the current file with $VISUAL or $EDITOR.
|
||||
V Print version number of "less".
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
OOPPTTIIOONNSS
|
||||
|
||||
Most options may be changed either on the command line,
|
||||
or from within less by using the - or -- command.
|
||||
Options may be given in one of two forms: either a single
|
||||
character preceded by a -, or a name preceded by --.
|
||||
|
||||
-? ........ --help
|
||||
Display help (from command line).
|
||||
-a ........ --search-skip-screen
|
||||
Search skips current screen.
|
||||
-A ........ --SEARCH-SKIP-SCREEN
|
||||
Search starts just after target line.
|
||||
-b [_N] .... --buffers=[_N]
|
||||
Number of buffers.
|
||||
-B ........ --auto-buffers
|
||||
Don't automatically allocate buffers for pipes.
|
||||
-c ........ --clear-screen
|
||||
Repaint by clearing rather than scrolling.
|
||||
-d ........ --dumb
|
||||
Dumb terminal.
|
||||
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
|
||||
Set screen colors.
|
||||
-e -E .... --quit-at-eof --QUIT-AT-EOF
|
||||
Quit at end of file.
|
||||
-f ........ --force
|
||||
Force open non-regular files.
|
||||
-F ........ --quit-if-one-screen
|
||||
Quit if entire file fits on first screen.
|
||||
-g ........ --hilite-search
|
||||
Highlight only last match for searches.
|
||||
-G ........ --HILITE-SEARCH
|
||||
Don't highlight any matches for searches.
|
||||
-h [_N] .... --max-back-scroll=[_N]
|
||||
Backward scroll limit.
|
||||
-i ........ --ignore-case
|
||||
Ignore case in searches that do not contain uppercase.
|
||||
-I ........ --IGNORE-CASE
|
||||
Ignore case in all searches.
|
||||
-j [_N] .... --jump-target=[_N]
|
||||
Screen position of target lines.
|
||||
-J ........ --status-column
|
||||
Display a status column at left edge of screen.
|
||||
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
|
||||
Use a lesskey file.
|
||||
-K ........ --quit-on-intr
|
||||
Exit less in response to ctrl-C.
|
||||
-L ........ --no-lessopen
|
||||
Ignore the LESSOPEN environment variable.
|
||||
-m -M .... --long-prompt --LONG-PROMPT
|
||||
Set prompt style.
|
||||
-n -N .... --line-numbers --LINE-NUMBERS
|
||||
Don't use line numbers.
|
||||
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
|
||||
Copy to log file (standard input only).
|
||||
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
|
||||
Copy to log file (unconditionally overwrite).
|
||||
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
|
||||
Start at pattern (from command line).
|
||||
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
|
||||
Define new prompt.
|
||||
-q -Q .... --quiet --QUIET --silent --SILENT
|
||||
Quiet the terminal bell.
|
||||
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
|
||||
Output "raw" control characters.
|
||||
-s ........ --squeeze-blank-lines
|
||||
Squeeze multiple blank lines.
|
||||
-S ........ --chop-long-lines
|
||||
Chop (truncate) long lines rather than wrapping.
|
||||
-t [_t_a_g] .. --tag=[_t_a_g]
|
||||
Find a tag.
|
||||
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
|
||||
Use an alternate tags file.
|
||||
-u -U .... --underline-special --UNDERLINE-SPECIAL
|
||||
Change handling of backspaces.
|
||||
-V ........ --version
|
||||
Display the version number of "less".
|
||||
-w ........ --hilite-unread
|
||||
Highlight first new line after forward-screen.
|
||||
-W ........ --HILITE-UNREAD
|
||||
Highlight first new line after any forward movement.
|
||||
-x [_N[,...]] --tabs=[_N[,...]]
|
||||
Set tab stops.
|
||||
-X ........ --no-init
|
||||
Don't use termcap init/deinit strings.
|
||||
-y [_N] .... --max-forw-scroll=[_N]
|
||||
Forward scroll limit.
|
||||
-z [_N] .... --window=[_N]
|
||||
Set size of window.
|
||||
-" [_c[_c]] . --quotes=[_c[_c]]
|
||||
Set shell quote characters.
|
||||
-~ ........ --tilde
|
||||
Don't display tildes after end of file.
|
||||
-# [_N] .... --shift=[_N]
|
||||
Set horizontal scroll amount (0 = one half screen width).
|
||||
--file-size
|
||||
Automatically determine the size of the input file.
|
||||
--follow-name
|
||||
The F command changes files if the input file is renamed.
|
||||
--incsearch
|
||||
Search file as each pattern character is typed in.
|
||||
--line-num-width=N
|
||||
Set the width of the -N line number field to N characters.
|
||||
--mouse
|
||||
Enable mouse input.
|
||||
--no-keypad
|
||||
Don't send termcap keypad init/deinit strings.
|
||||
--no-histdups
|
||||
Remove duplicates from command history.
|
||||
--rscroll=C
|
||||
Set the character used to mark truncated lines.
|
||||
--save-marks
|
||||
Retain marks across invocations of less.
|
||||
--status-col-width=N
|
||||
Set the width of the -J status column to N characters.
|
||||
--use-backslash
|
||||
Subsequent options use backslash as escape char.
|
||||
--use-color
|
||||
Enables colored text.
|
||||
--wheel-lines=N
|
||||
Each click of the mouse wheel moves N lines.
|
||||
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
LLIINNEE EEDDIITTIINNGG
|
||||
|
||||
These keys can be used to edit text being entered
|
||||
on the "command line" at the bottom of the screen.
|
||||
|
||||
RightArrow ..................... ESC-l ... Move cursor right one character.
|
||||
LeftArrow ...................... ESC-h ... Move cursor left one character.
|
||||
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
|
||||
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
|
||||
HOME ........................... ESC-0 ... Move cursor to start of line.
|
||||
END ............................ ESC-$ ... Move cursor to end of line.
|
||||
BACKSPACE ................................ Delete char to left of cursor.
|
||||
DELETE ......................... ESC-x ... Delete char under cursor.
|
||||
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
|
||||
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
|
||||
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
|
||||
UpArrow ........................ ESC-k ... Retrieve previous command line.
|
||||
DownArrow ...................... ESC-j ... Retrieve next command line.
|
||||
TAB ...................................... Complete filename & cycle.
|
||||
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
|
||||
ctrl-L ................................... Complete filename, list all.
|
||||
@@ -0,0 +1,258 @@
|
||||
|
||||
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
||||
|
||||
Commands marked with * may be preceded by a number, _N.
|
||||
Notes in parentheses indicate the behavior if _N is given.
|
||||
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
||||
|
||||
h H Display this help.
|
||||
q :q Q :Q ZZ Exit.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMOOVVIINNGG
|
||||
|
||||
e ^E j ^N CR * Forward one line (or _N lines).
|
||||
y ^Y k ^K ^P * Backward one line (or _N lines).
|
||||
f ^F ^V SPACE * Forward one window (or _N lines).
|
||||
b ^B ESC-v * Backward one window (or _N lines).
|
||||
z * Forward one window (and set window to _N).
|
||||
w * Backward one window (and set window to _N).
|
||||
ESC-SPACE * Forward one window, but don't stop at end-of-file.
|
||||
d ^D * Forward one half-window (and set half-window to _N).
|
||||
u ^U * Backward one half-window (and set half-window to _N).
|
||||
ESC-) RightArrow * Right one half screen width (or _N positions).
|
||||
ESC-( LeftArrow * Left one half screen width (or _N positions).
|
||||
ESC-} ^RightArrow Right to last column displayed.
|
||||
ESC-{ ^LeftArrow Left to first column.
|
||||
F Forward forever; like "tail -f".
|
||||
ESC-F Like F but stop when search pattern is found.
|
||||
r ^R ^L Repaint screen.
|
||||
R Repaint screen, discarding buffered input.
|
||||
---------------------------------------------------
|
||||
Default "window" is the screen height.
|
||||
Default "half-window" is half of the screen height.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
SSEEAARRCCHHIINNGG
|
||||
|
||||
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
|
||||
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
|
||||
n * Repeat previous search (for _N-th occurrence).
|
||||
N * Repeat previous search in reverse direction.
|
||||
ESC-n * Repeat previous search, spanning files.
|
||||
ESC-N * Repeat previous search, reverse dir. & spanning files.
|
||||
ESC-u Undo (toggle) search highlighting.
|
||||
ESC-U Clear search highlighting.
|
||||
&_p_a_t_t_e_r_n * Display only matching lines.
|
||||
---------------------------------------------------
|
||||
A search pattern may begin with one or more of:
|
||||
^N or ! Search for NON-matching lines.
|
||||
^E or * Search multiple files (pass thru END OF FILE).
|
||||
^F or @ Start search at FIRST file (for /) or last file (for ?).
|
||||
^K Highlight matches, but don't move (KEEP position).
|
||||
^R Don't use REGULAR EXPRESSIONS.
|
||||
^W WRAP search if no match found.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
JJUUMMPPIINNGG
|
||||
|
||||
g < ESC-< * Go to first line in file (or line _N).
|
||||
G > ESC-> * Go to last line in file (or line _N).
|
||||
p % * Go to beginning of file (or _N percent into file).
|
||||
t * Go to the (_N-th) next tag.
|
||||
T * Go to the (_N-th) previous tag.
|
||||
{ ( [ * Find close bracket } ) ].
|
||||
} ) ] * Find open bracket { ( [.
|
||||
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
|
||||
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
|
||||
---------------------------------------------------
|
||||
Each "find close bracket" command goes forward to the close bracket
|
||||
matching the (_N-th) open bracket in the top line.
|
||||
Each "find open bracket" command goes backward to the open bracket
|
||||
matching the (_N-th) close bracket in the bottom line.
|
||||
|
||||
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
|
||||
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
|
||||
'_<_l_e_t_t_e_r_> Go to a previously marked position.
|
||||
'' Go to the previous position.
|
||||
^X^X Same as '.
|
||||
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
|
||||
---------------------------------------------------
|
||||
A mark is any upper-case or lower-case letter.
|
||||
Certain marks are predefined:
|
||||
^ means beginning of the file
|
||||
$ means end of the file
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CCHHAANNGGIINNGG FFIILLEESS
|
||||
|
||||
:e [_f_i_l_e] Examine a new file.
|
||||
^X^V Same as :e.
|
||||
:n * Examine the (_N-th) next file from the command line.
|
||||
:p * Examine the (_N-th) previous file from the command line.
|
||||
:x * Examine the first (or _N-th) file from the command line.
|
||||
:d Delete the current file from the command line list.
|
||||
= ^G :f Print current file name.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
|
||||
|
||||
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
|
||||
--_<_n_a_m_e_> Toggle a command line option, by name.
|
||||
__<_f_l_a_g_> Display the setting of a command line option.
|
||||
___<_n_a_m_e_> Display the setting of an option, by name.
|
||||
+_c_m_d Execute the less cmd each time a new file is examined.
|
||||
|
||||
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|
||||
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
|
||||
s _f_i_l_e Save input to a file.
|
||||
v Edit the current file with $VISUAL or $EDITOR.
|
||||
V Print version number of "less".
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
OOPPTTIIOONNSS
|
||||
|
||||
Most options may be changed either on the command line,
|
||||
or from within less by using the - or -- command.
|
||||
Options may be given in one of two forms: either a single
|
||||
character preceded by a -, or a name preceded by --.
|
||||
|
||||
-? ........ --help
|
||||
Display help (from command line).
|
||||
-a ........ --search-skip-screen
|
||||
Search skips current screen.
|
||||
-A ........ --SEARCH-SKIP-SCREEN
|
||||
Search starts just after target line.
|
||||
-b [_N] .... --buffers=[_N]
|
||||
Number of buffers.
|
||||
-B ........ --auto-buffers
|
||||
Don't automatically allocate buffers for pipes.
|
||||
-c ........ --clear-screen
|
||||
Repaint by clearing rather than scrolling.
|
||||
-d ........ --dumb
|
||||
Dumb terminal.
|
||||
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
|
||||
Set screen colors.
|
||||
-e -E .... --quit-at-eof --QUIT-AT-EOF
|
||||
Quit at end of file.
|
||||
-f ........ --force
|
||||
Force open non-regular files.
|
||||
-F ........ --quit-if-one-screen
|
||||
Quit if entire file fits on first screen.
|
||||
-g ........ --hilite-search
|
||||
Highlight only last match for searches.
|
||||
-G ........ --HILITE-SEARCH
|
||||
Don't highlight any matches for searches.
|
||||
-h [_N] .... --max-back-scroll=[_N]
|
||||
Backward scroll limit.
|
||||
-i ........ --ignore-case
|
||||
Ignore case in searches that do not contain uppercase.
|
||||
-I ........ --IGNORE-CASE
|
||||
Ignore case in all searches.
|
||||
-j [_N] .... --jump-target=[_N]
|
||||
Screen position of target lines.
|
||||
-J ........ --status-column
|
||||
Display a status column at left edge of screen.
|
||||
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
|
||||
Use a lesskey file.
|
||||
-K ........ --quit-on-intr
|
||||
Exit less in response to ctrl-C.
|
||||
-L ........ --no-lessopen
|
||||
Ignore the LESSOPEN environment variable.
|
||||
-m -M .... --long-prompt --LONG-PROMPT
|
||||
Set prompt style.
|
||||
-n -N .... --line-numbers --LINE-NUMBERS
|
||||
Don't use line numbers.
|
||||
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
|
||||
Copy to log file (standard input only).
|
||||
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
|
||||
Copy to log file (unconditionally overwrite).
|
||||
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
|
||||
Start at pattern (from command line).
|
||||
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
|
||||
Define new prompt.
|
||||
-q -Q .... --quiet --QUIET --silent --SILENT
|
||||
Quiet the terminal bell.
|
||||
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
|
||||
Output "raw" control characters.
|
||||
-s ........ --squeeze-blank-lines
|
||||
Squeeze multiple blank lines.
|
||||
-S ........ --chop-long-lines
|
||||
Chop (truncate) long lines rather than wrapping.
|
||||
-t [_t_a_g] .. --tag=[_t_a_g]
|
||||
Find a tag.
|
||||
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
|
||||
Use an alternate tags file.
|
||||
-u -U .... --underline-special --UNDERLINE-SPECIAL
|
||||
Change handling of backspaces.
|
||||
-V ........ --version
|
||||
Display the version number of "less".
|
||||
-w ........ --hilite-unread
|
||||
Highlight first new line after forward-screen.
|
||||
-W ........ --HILITE-UNREAD
|
||||
Highlight first new line after any forward movement.
|
||||
-x [_N[,...]] --tabs=[_N[,...]]
|
||||
Set tab stops.
|
||||
-X ........ --no-init
|
||||
Don't use termcap init/deinit strings.
|
||||
-y [_N] .... --max-forw-scroll=[_N]
|
||||
Forward scroll limit.
|
||||
-z [_N] .... --window=[_N]
|
||||
Set size of window.
|
||||
-" [_c[_c]] . --quotes=[_c[_c]]
|
||||
Set shell quote characters.
|
||||
-~ ........ --tilde
|
||||
Don't display tildes after end of file.
|
||||
-# [_N] .... --shift=[_N]
|
||||
Set horizontal scroll amount (0 = one half screen width).
|
||||
--file-size
|
||||
Automatically determine the size of the input file.
|
||||
--follow-name
|
||||
The F command changes files if the input file is renamed.
|
||||
--incsearch
|
||||
Search file as each pattern character is typed in.
|
||||
--line-num-width=N
|
||||
Set the width of the -N line number field to N characters.
|
||||
--mouse
|
||||
Enable mouse input.
|
||||
--no-keypad
|
||||
Don't send termcap keypad init/deinit strings.
|
||||
--no-histdups
|
||||
Remove duplicates from command history.
|
||||
--rscroll=C
|
||||
Set the character used to mark truncated lines.
|
||||
--save-marks
|
||||
Retain marks across invocations of less.
|
||||
--status-col-width=N
|
||||
Set the width of the -J status column to N characters.
|
||||
--use-backslash
|
||||
Subsequent options use backslash as escape char.
|
||||
--use-color
|
||||
Enables colored text.
|
||||
--wheel-lines=N
|
||||
Each click of the mouse wheel moves N lines.
|
||||
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
LLIINNEE EEDDIITTIINNGG
|
||||
|
||||
These keys can be used to edit text being entered
|
||||
on the "command line" at the bottom of the screen.
|
||||
|
||||
RightArrow ..................... ESC-l ... Move cursor right one character.
|
||||
LeftArrow ...................... ESC-h ... Move cursor left one character.
|
||||
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
|
||||
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
|
||||
HOME ........................... ESC-0 ... Move cursor to start of line.
|
||||
END ............................ ESC-$ ... Move cursor to end of line.
|
||||
BACKSPACE ................................ Delete char to left of cursor.
|
||||
DELETE ......................... ESC-x ... Delete char under cursor.
|
||||
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
|
||||
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
|
||||
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
|
||||
UpArrow ........................ ESC-k ... Retrieve previous command line.
|
||||
DownArrow ...................... ESC-j ... Retrieve next command line.
|
||||
TAB ...................................... Complete filename & cycle.
|
||||
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
|
||||
ctrl-L ................................... Complete filename, list all.
|
||||
@@ -0,0 +1,14 @@
|
||||
tableName | syncMode | recordsProcessed | recordsInserted | recordsSkipped | recordsFailed | createdAt
|
||||
--------------+-------------+------------------+-----------------+----------------+---------------+-------------------------
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:57.769
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:33.239
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:10:04.175
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:09:40.038
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:09:15.606
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:08:50.332
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:08:22.615
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:56.832
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:31.662
|
||||
Product Data | incremental | 0 | 0 | 0 | 0 | 2026-04-21 23:07:06.055
|
||||
(10 rows)
|
||||
|
||||
@@ -621,7 +621,7 @@
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||
|
||||
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||
|
||||
@@ -841,7 +841,7 @@
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
|
||||
+376
-110
@@ -666,6 +666,7 @@ model Member {
|
||||
approvedOpportunities Opportunity[] @relation("OpportunityApprovedBy")
|
||||
rejectedOpportunities Opportunity[] @relation("OpportunityRejectedBy")
|
||||
opportunityMembers OpportunityMember[] @relation("OpportunityMemberToMember")
|
||||
memberType MemberType? @relation(fields: [memberTypeRecId], references: [memberTypeRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@map("Member")
|
||||
@@schema("dbo")
|
||||
@@ -1740,10 +1741,13 @@ model ActivityType {
|
||||
description String @map("Description") @db.NVarChar(50)
|
||||
hoursMin Decimal? @map("Hours_Min") @db.Decimal(18, 2)
|
||||
hoursMax Decimal? @map("Hours_Max") @db.Decimal(18, 2)
|
||||
defaultFlag Boolean @map("Default_Flag")
|
||||
multiplierFlag Boolean @map("Multiplier_Flag")
|
||||
rate Decimal? @map("Rate") @db.Decimal(18, 2)
|
||||
rateType String? @map("Rate_Type") @db.Char(1)
|
||||
|
||||
defaultFlag Boolean @map("Default_Flag")
|
||||
multiplierFlag Boolean @map("Multiplier_Flag")
|
||||
|
||||
rate Decimal? @map("Rate") @db.Decimal(18, 2)
|
||||
rateType String? @map("Rate_Type") @db.Char(1)
|
||||
|
||||
inactiveFlag Boolean @map("Inactive_Flag")
|
||||
invoiceFlag Boolean @map("Invoice_Flag")
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime2
|
||||
@@ -2069,45 +2073,63 @@ model Country {
|
||||
// =====================
|
||||
|
||||
model SoActivity {
|
||||
soActivityRecId Int @id @map("SO_Activity_Recid")
|
||||
opportunityRecId Int? @map("Opportunity_Recid")
|
||||
assignTo String @map("Assign_To") @db.NVarChar(15)
|
||||
assignedBy String @map("Assigned_By") @db.NVarChar(15)
|
||||
companyRecId Int? @map("Company_RecID")
|
||||
soActivityTypeRecId Int? @map("SO_Activity_Type_RecID")
|
||||
subject String? @map("Subject") @db.NVarChar(100)
|
||||
soReferenceRecId Int? @map("SO_Reference_RecID")
|
||||
dateEntered DateTime @map("Date_Entered") @db.DateTime
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
contactRecId Int? @map("Contact_RecID")
|
||||
contactName String? @map("Contact_Name") @db.NVarChar(62)
|
||||
closeFlag Boolean @map("Close_Flag")
|
||||
dateClosed DateTime? @map("Date_Closed") @db.DateTime
|
||||
closedBy String? @map("Closed_By") @db.NVarChar(15)
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime
|
||||
notifyCompleteFlag Boolean @map("Notify_Complete_Flag")
|
||||
notificationSentFlag Boolean @map("Notification_Sent_Flag")
|
||||
srServiceRecId Int? @map("SR_Service_RecID")
|
||||
agrHeaderRecId Int? @map("AGR_Header_RecID")
|
||||
marketingCampaignRecId Int? @map("Marketing_Campaign_RecID")
|
||||
assignToRecId Int @map("assignto_recid")
|
||||
assignByRecId Int? @map("assignby_recid")
|
||||
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
|
||||
srLocationRecId Int? @map("SR_Location_RecID")
|
||||
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
|
||||
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
|
||||
automated Boolean @map("Automated")
|
||||
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
|
||||
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime
|
||||
lastUpdatedUTC DateTime @map("Last_Update_UTC") @db.DateTime
|
||||
dateClosedUtc DateTime? @map("Date_Closed_UTC") @db.DateTime
|
||||
soActStatusRecId Int @map("so_act_status_recid")
|
||||
currencyRecId Int @map("Currency_RecID")
|
||||
id String? @map("Id") @db.UniqueIdentifier
|
||||
subject String? @map("Subject") @db.NVarChar(100)
|
||||
|
||||
opportunity Opportunity? @relation(fields: [opportunityRecId], references: [opportunityRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
soActivityRecId Int @id @map("SO_Activity_Recid")
|
||||
opportunityRecId Int? @map("Opportunity_Recid")
|
||||
companyRecId Int? @map("Company_RecID")
|
||||
|
||||
assignTo String @map("Assign_To") @db.NVarChar(15)
|
||||
assignedBy String @map("Assigned_By") @db.NVarChar(15)
|
||||
|
||||
soActivityTypeRecId Int? @map("SO_Activity_Type_RecID")
|
||||
soReferenceRecId Int? @map("SO_Reference_RecID")
|
||||
|
||||
dateEntered DateTime @map("Date_Entered") @db.DateTime
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
|
||||
contactRecId Int? @map("Contact_RecID")
|
||||
contactName String? @map("Contact_Name") @db.NVarChar(62)
|
||||
|
||||
closeFlag Boolean @map("Close_Flag")
|
||||
dateClosed DateTime? @map("Date_Closed") @db.DateTime
|
||||
closedBy String? @map("Closed_By") @db.NVarChar(15)
|
||||
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime
|
||||
|
||||
notifyCompleteFlag Boolean @map("Notify_Complete_Flag")
|
||||
notificationSentFlag Boolean @map("Notification_Sent_Flag")
|
||||
|
||||
srServiceRecId Int? @map("SR_Service_RecID")
|
||||
|
||||
agrHeaderRecId Int? @map("AGR_Header_RecID")
|
||||
marketingCampaignRecId Int? @map("Marketing_Campaign_RecID")
|
||||
|
||||
assignToRecId Int @map("assignto_recid")
|
||||
assignByRecId Int? @map("assignby_recid")
|
||||
|
||||
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
|
||||
srLocationRecId Int? @map("SR_Location_RecID")
|
||||
|
||||
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
|
||||
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
|
||||
|
||||
automated Boolean @map("Automated")
|
||||
|
||||
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
|
||||
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime
|
||||
lastUpdatedUTC DateTime @map("Last_Update_UTC") @db.DateTime
|
||||
dateClosedUtc DateTime? @map("Date_Closed_UTC") @db.DateTime
|
||||
|
||||
soActStatusRecId Int @map("so_act_status_recid")
|
||||
currencyRecId Int @map("Currency_RecID")
|
||||
|
||||
id String? @map("Id") @db.UniqueIdentifier
|
||||
|
||||
opportunity Opportunity? @relation(fields: [opportunityRecId], references: [opportunityRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
notes SoActivityNotes?
|
||||
|
||||
@@map("SO_Activity")
|
||||
@@schema("dbo")
|
||||
@@ -2284,83 +2306,84 @@ model ScheduleStatus {
|
||||
}
|
||||
|
||||
model ScheduleType {
|
||||
scheduleTypeRecId Int @id @map("Schedule_Type_RecID")
|
||||
tableReference String? @map("Table_Reference") @db.NVarChar(50)
|
||||
description String? @map("Description") @db.NVarChar(50)
|
||||
displayColor String? @map("Display_Color") @db.NVarChar(30)
|
||||
moduleId String? @map("Module_ID") @db.Char(2)
|
||||
systemFlag Boolean @map("System_Flag")
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime2
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
displayFlag Boolean @map("Display_Flag")
|
||||
xrefMbrTable String? @map("Xref_Mbr_Table") @db.NVarChar(50)
|
||||
scheduleTypeId String? @map("Schedule_Type_ID") @db.Char(1)
|
||||
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
|
||||
srLocationRecId Int? @map("SR_Location_RecID")
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
|
||||
id String @map("Id") @db.UniqueIdentifier
|
||||
scheduleTypeRecId Int @id @map("Schedule_Type_RecID")
|
||||
tableReference String? @map("Table_Reference") @db.NVarChar(50)
|
||||
description String? @map("Description") @db.NVarChar(50)
|
||||
displayColor String? @map("Display_Color") @db.NVarChar(30)
|
||||
moduleId String? @map("Module_ID") @db.Char(2)
|
||||
systemFlag Boolean @map("System_Flag")
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime2
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
displayFlag Boolean @map("Display_Flag")
|
||||
xrefMbrTable String? @map("Xref_Mbr_Table") @db.NVarChar(50)
|
||||
scheduleTypeId String? @map("Schedule_Type_ID") @db.Char(1)
|
||||
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
|
||||
srLocationRecId Int? @map("SR_Location_RecID")
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
|
||||
id String @map("Id") @db.UniqueIdentifier
|
||||
|
||||
schedules Schedule[]
|
||||
schedules Schedule[]
|
||||
teChargeCode TeChargeCode? @relation(fields: [teChargeCodeRecId], references: [teChargeCodeRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@map("Schedule_Type")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
model Schedule {
|
||||
scheduleRecId Int @id @map("Schedule_RecID")
|
||||
recId Int? @map("RecID")
|
||||
scheduleTypeRecId Int @map("Schedule_Type_RecID")
|
||||
memberId String? @map("Member_ID") @db.NVarChar(15)
|
||||
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
|
||||
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
|
||||
closeFlag Boolean @map("close_flag")
|
||||
hoursEstimated Decimal? @map("Hours_Estimated") @db.Decimal(18, 2)
|
||||
lastUpdate DateTime? @map("Last_Update") @db.DateTime
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
syncable Boolean @map("Syncable")
|
||||
lastSync DateTime? @map("Last_Sync") @db.DateTime
|
||||
exchangeGuid String? @map("Exchange_GUID") @db.VarChar(4000)
|
||||
reminderFlag Boolean @map("Reminder_Flag")
|
||||
reminderMinutes Int? @map("Reminder_Minutes")
|
||||
allDayFlag Boolean @map("All_Day_Flag")
|
||||
duration Int? @map("Duration")
|
||||
enteredByRecId Int? @map("Entered_By_RecID")
|
||||
xrefMbrRecId Int? @map("Xref_Mbr_RecID")
|
||||
percentSched Int? @map("Percent_Sched")
|
||||
hoursSched Decimal? @map("Hours_Sched") @db.Decimal(18, 2)
|
||||
scheduleStatusRecId Int? @map("Schedule_Status_RecID")
|
||||
hoursPerDay Decimal? @map("Hours_Per_Day") @db.Decimal(18, 2)
|
||||
ackFlag Boolean? @map("Ack_Flag")
|
||||
ackMemberRecId Int? @map("Ack_Member_RecID")
|
||||
ackDate DateTime? @map("Ack_Date") @db.DateTime
|
||||
closeMemberRecId Int? @map("Close_Member_RecID")
|
||||
closeDate DateTime? @map("Close_Date") @db.DateTime
|
||||
billableFlag Boolean? @map("Billable_Flag")
|
||||
dateEntered DateTime? @map("Date_Entered") @db.DateTime
|
||||
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
|
||||
srLocationRecId Int? @map("SR_Location_RecID")
|
||||
scheduleSpanRecId Int? @map("Schedule_Span_RecID")
|
||||
meetingFlag Boolean? @map("Meeting_Flag")
|
||||
recurringFlag Boolean? @map("Recurring_Flag")
|
||||
ackDateUtc DateTime? @map("Ack_Date_UTC") @db.DateTime
|
||||
dateEnteredUtc DateTime? @map("Date_Entered_UTC") @db.DateTime
|
||||
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
|
||||
closeDateUtc DateTime? @map("Close_Date_UTC") @db.DateTime
|
||||
enteredBy String? @map("Entered_By") @db.NVarChar(15)
|
||||
acknowledgedBy String? @map("Acknowledged_By") @db.NVarChar(15)
|
||||
closedBy String? @map("Closed_By") @db.NVarChar(15)
|
||||
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
|
||||
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
|
||||
scheduleDesc String? @map("Schedule_Desc") @db.NVarChar(250)
|
||||
privateFlag Boolean @map("Private_Flag")
|
||||
notifyType String? @map("NotifyType") @db.NVarChar(2)
|
||||
scheduleRecId Int @id @map("Schedule_RecID")
|
||||
recId Int? @map("RecID")
|
||||
scheduleTypeRecId Int @map("Schedule_Type_RecID")
|
||||
memberId String? @map("Member_ID") @db.NVarChar(15)
|
||||
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
|
||||
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
|
||||
closeFlag Boolean @map("close_flag")
|
||||
hoursEstimated Decimal? @map("Hours_Estimated") @db.Decimal(18, 2)
|
||||
lastUpdate DateTime? @map("Last_Update") @db.DateTime
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
syncable Boolean @map("Syncable")
|
||||
lastSync DateTime? @map("Last_Sync") @db.DateTime
|
||||
exchangeGuid String? @map("Exchange_GUID") @db.VarChar(4000)
|
||||
reminderFlag Boolean @map("Reminder_Flag")
|
||||
reminderMinutes Int? @map("Reminder_Minutes")
|
||||
allDayFlag Boolean @map("All_Day_Flag")
|
||||
duration Int? @map("Duration")
|
||||
enteredByRecId Int? @map("Entered_By_RecID")
|
||||
xrefMbrRecId Int? @map("Xref_Mbr_RecID")
|
||||
percentSched Int? @map("Percent_Sched")
|
||||
hoursSched Decimal? @map("Hours_Sched") @db.Decimal(18, 2)
|
||||
scheduleStatusRecId Int? @map("Schedule_Status_RecID")
|
||||
hoursPerDay Decimal? @map("Hours_Per_Day") @db.Decimal(18, 2)
|
||||
ackFlag Boolean? @map("Ack_Flag")
|
||||
ackMemberRecId Int? @map("Ack_Member_RecID")
|
||||
ackDate DateTime? @map("Ack_Date") @db.DateTime
|
||||
closeMemberRecId Int? @map("Close_Member_RecID")
|
||||
closeDate DateTime? @map("Close_Date") @db.DateTime
|
||||
billableFlag Boolean? @map("Billable_Flag")
|
||||
dateEntered DateTime? @map("Date_Entered") @db.DateTime
|
||||
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
|
||||
srLocationRecId Int? @map("SR_Location_RecID")
|
||||
scheduleSpanRecId Int? @map("Schedule_Span_RecID")
|
||||
meetingFlag Boolean? @map("Meeting_Flag")
|
||||
recurringFlag Boolean? @map("Recurring_Flag")
|
||||
ackDateUtc DateTime? @map("Ack_Date_UTC") @db.DateTime
|
||||
dateEnteredUtc DateTime? @map("Date_Entered_UTC") @db.DateTime
|
||||
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
|
||||
closeDateUtc DateTime? @map("Close_Date_UTC") @db.DateTime
|
||||
enteredBy String? @map("Entered_By") @db.NVarChar(15)
|
||||
acknowledgedBy String? @map("Acknowledged_By") @db.NVarChar(15)
|
||||
closedBy String? @map("Closed_By") @db.NVarChar(15)
|
||||
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
|
||||
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
|
||||
scheduleDesc String? @map("Schedule_Desc") @db.NVarChar(250)
|
||||
privateFlag Boolean @map("Private_Flag")
|
||||
notifyType String? @map("NotifyType") @db.NVarChar(2)
|
||||
|
||||
details ScheduleDetail[]
|
||||
status ScheduleStatus? @relation(fields: [scheduleStatusRecId], references: [scheduleStatusRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
type ScheduleType @relation(fields: [scheduleTypeRecId], references: [scheduleTypeRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
span ScheduleSpan? @relation(fields: [scheduleSpanRecId], references: [scheduleSpanRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
status ScheduleStatus? @relation(fields: [scheduleStatusRecId], references: [scheduleStatusRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
type ScheduleType @relation(fields: [scheduleTypeRecId], references: [scheduleTypeRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
span ScheduleSpan? @relation(fields: [scheduleSpanRecId], references: [scheduleSpanRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@map("Schedule")
|
||||
@@schema("dbo")
|
||||
@@ -2398,3 +2421,246 @@ model ScheduleDetail {
|
||||
@@map("Schedule_Detail")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
// =====================
|
||||
// TIME
|
||||
// =====================
|
||||
|
||||
model TimeEntry {
|
||||
companyRecId Int @map("Company_RecID")
|
||||
timeRecId Int @map("Time_RecID")
|
||||
enteredBy String? @map("Entered_By") @db.NVarChar(15)
|
||||
memberId String? @map("Member_ID") @db.NVarChar(15)
|
||||
|
||||
dateStart DateTime? @map("Date_Start") @db.DateTime
|
||||
timeStart DateTime? @map("Time_Start") @db.DateTime
|
||||
timeEnd DateTime? @map("Time_End") @db.DateTime
|
||||
hourlyRate Float? @map("Hourly_Rate") @db.SmallMoney
|
||||
hoursBill Decimal @map("Hours_Bill") @db.Decimal(18, 2)
|
||||
|
||||
invoiceFlag Boolean @map("Invoice_Flag")
|
||||
|
||||
lastUpdate DateTime? @map("Last_Update") @db.DateTime
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
|
||||
pmProjectRecId Int? @map("PM_Project_RecID")
|
||||
|
||||
hoursActual Decimal @map("Hours_Actual") @db.Decimal(18, 2)
|
||||
billableFlag Boolean @map("Billable_Flag")
|
||||
billingLogRecId Int? @map("Billing_Log_RecID")
|
||||
|
||||
srServiceRecId Int? @map("SR_Service_RecID")
|
||||
|
||||
activityTypeRecId Int? @map("Activity_Type_RecID")
|
||||
activityClassRecId Int? @map("Activity_Class_RecID")
|
||||
|
||||
teStatusId Int @map("TE_Status_ID") @db.SmallInt
|
||||
timeSheetRecId Int? @map("Time_Sheet_RecID")
|
||||
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
|
||||
|
||||
billingUnitRecId Int @map("Billing_Unit_RecID")
|
||||
ownerLevelRecId Int @map("Owner_Level_RecID")
|
||||
|
||||
memberRecId Int? @map("Member_RecID")
|
||||
|
||||
hoursInvoiced Decimal? @map("Hours_Invoiced") @db.Decimal(18, 2)
|
||||
adjustment Decimal @map("Adjustment") @db.Decimal(18, 2)
|
||||
|
||||
memberTypeRecId Int? @map("Member_Type_RecID")
|
||||
|
||||
agrAmount Decimal @map("Agr_Amount") @db.Decimal(18, 2)
|
||||
agrHeaderRecId Int? @map("Agr_Header_RecID")
|
||||
agrAdjustment Decimal? @map("Agr_Adjustment") @db.Decimal(18, 2)
|
||||
agrHours Decimal? @map("Agr_Hours") @db.Decimal(18, 2)
|
||||
agrMonth Int? @map("Agr_Month") @db.SmallInt
|
||||
agrYear Int? @map("Agr_Year") @db.SmallInt
|
||||
|
||||
standardRate Decimal? @map("Standard_Rate") @db.Decimal(18, 2)
|
||||
effectiveRate Decimal? @map("Effective_Rate") @db.Decimal(18, 2)
|
||||
|
||||
hoursDeduct Decimal? @map("Hours_Deduct") @db.Decimal(18, 2)
|
||||
|
||||
contactRecId Int? @map("Contact_RecID")
|
||||
|
||||
soActivityRecId Int? @map("SO_Activity_RecID")
|
||||
mobileGuid String @map("Mobile_GUID") @db.UniqueIdentifier
|
||||
billingSr Int? @map("billing_sr")
|
||||
|
||||
signatureRecId Int? @map("Signature_RecID")
|
||||
signatureHours Decimal? @map("Signature_Hours") @db.Decimal(18, 2)
|
||||
|
||||
exchangeHref String? @map("exchange_href") @db.NVarChar(300)
|
||||
teProblemFlag Boolean @map("TE_Problem_Flag")
|
||||
teResolutionFlag Boolean @map("TE_Resolution_Flag")
|
||||
teInternalAnalysisFlag Boolean @map("TE_InternalAnalysis_Flag")
|
||||
|
||||
documentFlag Boolean @map("Document_Flag")
|
||||
|
||||
dateFormat Int? @map("Date_Format")
|
||||
|
||||
dbTimestamp Bytes @map("DB_Timestamp") @db.VarBinary(8)
|
||||
timeStartUtc DateTime? @map("Time_Start_UTC") @db.DateTime
|
||||
timeEndUtc DateTime? @map("Time_End_UTC") @db.DateTime
|
||||
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime
|
||||
|
||||
overageRate Float? @map("Overage_Rate") @db.SmallMoney
|
||||
|
||||
extendedInvoiceAmount Decimal? @map("Extended_Invoice_Amount") @db.Decimal(31, 6)
|
||||
extendedBillAmount Decimal? @map("Extended_Bill_Amount") @db.Decimal(31, 6)
|
||||
|
||||
notificationHistory String @map("Notification_History") @db.VarChar(4000)
|
||||
mergedFlag Boolean @map("Merged_Flag")
|
||||
|
||||
internalNote String? @map("Internal_Note") @db.NVarChar(Max)
|
||||
reference String? @map("Reference") @db.NVarChar(100)
|
||||
|
||||
originalAuthor String? @map("Original_Author") @db.NVarChar(150)
|
||||
|
||||
costPerHour String @map("Cost_Per_Hour") @db.NVarChar(2000)
|
||||
overrideFlag Boolean @map("Override_Flag")
|
||||
issueFlag Boolean @map("Issue_Flag")
|
||||
|
||||
notes String? @map("Notes") @db.NVarChar(4000)
|
||||
notesMarkdown String? @map("Notes_Markdown") @db.NVarChar(Max)
|
||||
|
||||
chargeToRecId Int? @map("Charge_To_RecID")
|
||||
chargeToType String? @map("Charge_To_Type") @db.NVarChar(13)
|
||||
|
||||
teChargeCode TeChargeCode? @relation(fields: [teChargeCodeRecId], references: [teChargeCodeRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
memberType MemberType? @relation(fields: [memberTypeRecId], references: [memberTypeRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@id([companyRecId, timeRecId])
|
||||
@@map("Time_Entry")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
model SoActivityType {
|
||||
soActivityTypeRecId Int @id @map("SO_Activity_Type_RecID")
|
||||
soActivityTypeId String? @map("SO_Activity_Type_ID") @db.NVarChar(15)
|
||||
|
||||
description String? @map("Description") @db.NVarChar(50)
|
||||
|
||||
historyFlag Boolean @map("History_Flag")
|
||||
|
||||
updatedBy String @map("Updated_By") @db.NVarChar(15)
|
||||
cleanUpFlag Boolean @map("CleanUp_Flag")
|
||||
defaultFlag Boolean @map("Default_Flag")
|
||||
importFlag Boolean @map("Import_Flag")
|
||||
emailFlag Boolean @map("email_flag")
|
||||
memoFlag Boolean? @map("Memo_Flag")
|
||||
pointsValue Int? @map("Points_Value")
|
||||
inactiveFlag Boolean? @map("Inactive_Flag")
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
id String @map("Id") @db.UniqueIdentifier
|
||||
|
||||
@@map("SO_Activity_Type")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
model SoActStatus {
|
||||
soActStatusRecId Int @id @map("SO_Act_Status_RecID")
|
||||
description String? @map("Description") @db.NVarChar(30)
|
||||
|
||||
defaultFlag Boolean @map("Default_Flag")
|
||||
closedFlag Boolean @map("Closed_Flag")
|
||||
inactiveFlag Boolean @map("Inactive_Flag")
|
||||
spawnFollowupFlag Boolean @map("spawn_followup_flag")
|
||||
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime2
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
|
||||
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
|
||||
id String @map("Id") @db.UniqueIdentifier
|
||||
|
||||
@@map("SO_Act_Status")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
model SoActivityNotes {
|
||||
soActivityNotesRecId Int @id @map("SO_Activity_Notes_RecID")
|
||||
soActivityRecId Int @unique @map("SO_Activity_RecID")
|
||||
notes String @map("Notes") @db.NVarChar(Max)
|
||||
internalAnalysisFlag Boolean @map("Internal_Analysis_Flag")
|
||||
dateCreatedUtc DateTime @map("Date_Created_UTC") @db.DateTime2
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
updatedBy String @map("Updated_By") @db.NVarChar(15)
|
||||
|
||||
soActivity SoActivity @relation(fields: [soActivityRecId], references: [soActivityRecId], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@map("SO_Activity_Notes")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
// =====================
|
||||
// TIME ENTRY LOOKUPS
|
||||
// =====================
|
||||
|
||||
model TeStatus {
|
||||
teStatusRecId Int @id @map("TE_Status_RecID")
|
||||
teStatusId Int @map("TE_Status_ID") @db.SmallInt
|
||||
description String? @map("Description") @db.NVarChar(50)
|
||||
action String? @map("Action") @db.NVarChar(50)
|
||||
lastUpdatedUtc DateTime @map("Last_Updated_UTC") @db.DateTime2
|
||||
localeKeyRecId Int? @map("Locale_Key_RecID")
|
||||
|
||||
@@map("TE_Status")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
model TeChargeCode {
|
||||
teChargeCodeRecId Int @id @map("TE_Charge_Code_RecID")
|
||||
|
||||
description String? @map("Description") @db.NVarChar(50)
|
||||
|
||||
companyRecId Int? @map("Company_RecID")
|
||||
ownerLevelRecId Int? @map("Owner_Level_RecID")
|
||||
billingUnitRecId Int? @map("Billing_Unit_RecID")
|
||||
|
||||
activityClassRecId Int? @map("Activity_Class_RecID")
|
||||
activityTypeRecId Int? @map("Activity_Type_RecID")
|
||||
|
||||
expenseFlag Boolean @map("Expense_Flag")
|
||||
timeFlag Boolean @map("Time_Flag")
|
||||
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime2
|
||||
billableFlag Boolean @map("Billable_Flag")
|
||||
exTypeFlag Boolean @map("EX_Type_Flag")
|
||||
invoiceFlag Boolean? @map("Invoice_Flag")
|
||||
integrationXref String? @map("Integration_Xref") @db.NVarChar(50)
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
|
||||
id String @map("Id") @db.UniqueIdentifier
|
||||
|
||||
scheduleTypes ScheduleType[]
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
@@map("TE_Charge_Code")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
model MemberType {
|
||||
memberTypeRecId Int @id @map("Member_Type_RecID")
|
||||
description String? @map("Description") @db.NVarChar(30)
|
||||
inactiveFlag Boolean @map("Inactive_Flag")
|
||||
updatedBy String? @map("Updated_By") @db.NVarChar(15)
|
||||
lastUpdate DateTime @map("Last_Update") @db.DateTime2
|
||||
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
|
||||
enteredBy String @map("Entered_By") @db.NVarChar(15)
|
||||
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
|
||||
id String @map("Id") @db.UniqueIdentifier
|
||||
|
||||
members Member[]
|
||||
timeEntries TimeEntry[]
|
||||
|
||||
@@map("Member_Type")
|
||||
@@schema("dbo")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export { catalogCategoryTranslation } from "./translations/catalog-category";
|
||||
export { catalogSubcategoryTranslation } from "./translations/catalog-subcategory";
|
||||
export { catalogManufacturerTranslation } from "./translations/catalog-manufacturer";
|
||||
export { warehouseBinTranslation } from "./translations/warehouse-bin";
|
||||
export { warehouseTranslation } from "./translations/warehouse";
|
||||
export { productInventoryTranslation } from "./translations/product-inventory";
|
||||
export { productDataTranslation } from "./translations/product-data.ts";
|
||||
export { corporateLocationTranslation } from "./translations/corporate-location";
|
||||
@@ -31,6 +32,16 @@ export { scheduleTypeTranslation } from "./translations/schedule-type";
|
||||
export { scheduleSpanTranslation } from "./translations/schedule-span";
|
||||
export { scheduleTranslation } from "./translations/schedule";
|
||||
export { taxCodeTranslation } from "./translations/tax-code";
|
||||
export { timeEntryTranslation } from "./translations/time-entry";
|
||||
export { timeEntryStatusTranslation } from "./translations/time-entry-status";
|
||||
export { timeEntryChargeCodeTranslation } from "./translations/time-entry-charge-code";
|
||||
export { timeActivityClassTranslation } from "./translations/time-activity-class";
|
||||
export { timeActivityTypeTranslation } from "./translations/time-activity-type";
|
||||
export { activityTypeTranslation } from "./translations/activity-type";
|
||||
export { activityStatusTranslation } from "./translations/activity-status";
|
||||
export { activityTranslation } from "./translations/activity";
|
||||
export { activityNotesTranslation } from "./translations/activity-notes";
|
||||
export { cwMemberTypeTranslation } from "./translations/cw-member-type";
|
||||
|
||||
// Context type exports
|
||||
export type { TranslationContext } from "./translations/context";
|
||||
|
||||
@@ -37,8 +37,15 @@ import {
|
||||
scheduleTypeTranslation,
|
||||
scheduleSpanTranslation,
|
||||
scheduleTranslation,
|
||||
taxCodeTranslation,
|
||||
timeEntryTranslation,
|
||||
activityTypeTranslation,
|
||||
activityStatusTranslation,
|
||||
activityTranslation,
|
||||
activityNotesTranslation,
|
||||
userTranslation,
|
||||
warehouseBinTranslation,
|
||||
warehouseTranslation,
|
||||
type TranslationContext,
|
||||
} from "./index";
|
||||
import { Translation, SkipRowError } from "./translations/types";
|
||||
@@ -203,6 +210,9 @@ const refreshContextFromApi = async (
|
||||
context.scheduleStatusIds.clear();
|
||||
context.scheduleTypeIds.clear();
|
||||
context.scheduleSpanIds.clear();
|
||||
context.activityTypeIds.clear();
|
||||
context.activityStatusIds.clear();
|
||||
context.activityIds.clear();
|
||||
|
||||
const [
|
||||
users,
|
||||
@@ -215,6 +225,9 @@ const refreshContextFromApi = async (
|
||||
scheduleStatuses,
|
||||
scheduleTypes,
|
||||
scheduleSpans,
|
||||
activityTypes,
|
||||
activityStatuses,
|
||||
activities,
|
||||
] = await Promise.all([
|
||||
apiPrisma.user.findMany({
|
||||
select: {
|
||||
@@ -269,6 +282,21 @@ const refreshContextFromApi = async (
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.activityType.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.activityStatus.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.activity.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const defaultOpportunityType = await apiPrisma.opportunityType.findFirst({
|
||||
@@ -346,6 +374,18 @@ const refreshContextFromApi = async (
|
||||
context.scheduleSpanIds.add(span.id);
|
||||
}
|
||||
|
||||
for (const activityType of activityTypes) {
|
||||
context.activityTypeIds.add(activityType.id);
|
||||
}
|
||||
|
||||
for (const activityStatus of activityStatuses) {
|
||||
context.activityStatusIds.add(activityStatus.id);
|
||||
}
|
||||
|
||||
for (const activity of activities) {
|
||||
context.activityIds.add(activity.id);
|
||||
}
|
||||
|
||||
context.defaultOpportunityTypeId = defaultOpportunityType?.id ?? null;
|
||||
|
||||
// Optional context fed from env-provided maps.
|
||||
@@ -654,6 +694,13 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
warehouse: {
|
||||
sourceModel: "warehouse",
|
||||
targetModel: "warehouse",
|
||||
translation: warehouseTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
warehouseBin: {
|
||||
sourceModel: "warehouseBin",
|
||||
targetModel: "warehouseBin",
|
||||
@@ -761,6 +808,11 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
|
||||
closedFlag: true,
|
||||
},
|
||||
},
|
||||
soInterest: {
|
||||
select: {
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -813,6 +865,41 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
timeEntry: {
|
||||
sourceModel: "timeEntry",
|
||||
targetModel: "timeEntry",
|
||||
translation: timeEntryTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
soActivityType: {
|
||||
sourceModel: "soActivityType",
|
||||
targetModel: "activityType",
|
||||
translation: activityTypeTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
soActStatus: {
|
||||
sourceModel: "soActStatus",
|
||||
targetModel: "activityStatus",
|
||||
translation: activityStatusTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
soActivity: {
|
||||
sourceModel: "soActivity",
|
||||
targetModel: "activity",
|
||||
translation: activityTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdatedUTC",
|
||||
},
|
||||
soActivityNotes: {
|
||||
sourceModel: "soActivityNotes",
|
||||
targetModel: "activityNotes",
|
||||
translation: activityNotesTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
lastUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
};
|
||||
|
||||
return configMap[table] ?? null;
|
||||
@@ -972,8 +1059,8 @@ export async function syncTableUpdates(
|
||||
uniqueFields.length > 0
|
||||
? formatUniqueConstraintError(error, translatedData)
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "Unknown row sync error";
|
||||
? error.message
|
||||
: "Unknown row sync error";
|
||||
console.error(
|
||||
`Failed row in ${config.sourceModel} -> ${config.targetModel}:`,
|
||||
message
|
||||
@@ -984,8 +1071,7 @@ export async function syncTableUpdates(
|
||||
|
||||
if (failed > sampleErrorsPrinted) {
|
||||
console.error(
|
||||
`${config.sourceModel}: suppressed ${
|
||||
failed - sampleErrorsPrinted
|
||||
`${config.sourceModel}: suppressed ${failed - sampleErrorsPrinted
|
||||
} additional row errors`
|
||||
);
|
||||
}
|
||||
|
||||
+331
-34
@@ -39,8 +39,19 @@ import {
|
||||
scheduleSpanTranslation,
|
||||
scheduleTranslation,
|
||||
taxCodeTranslation,
|
||||
timeEntryTranslation,
|
||||
timeEntryStatusTranslation,
|
||||
timeEntryChargeCodeTranslation,
|
||||
timeActivityClassTranslation,
|
||||
timeActivityTypeTranslation,
|
||||
activityTypeTranslation,
|
||||
activityStatusTranslation,
|
||||
activityTranslation,
|
||||
activityNotesTranslation,
|
||||
cwMemberTypeTranslation,
|
||||
userTranslation,
|
||||
warehouseBinTranslation,
|
||||
warehouseTranslation,
|
||||
type TranslationContext,
|
||||
} from "./index";
|
||||
import { Translation, SkipRowError } from "./translations/types";
|
||||
@@ -79,20 +90,46 @@ const CRITICAL_INCREMENTAL_RECONCILE_TABLES = new Set([
|
||||
"Companies",
|
||||
"Company Addresses",
|
||||
"Contacts",
|
||||
"Opportunities",
|
||||
]);
|
||||
|
||||
const CRITICAL_CW_WATERMARK_TABLES = new Set([
|
||||
"Companies",
|
||||
"Company Addresses",
|
||||
"Contacts",
|
||||
"Opportunities",
|
||||
]);
|
||||
|
||||
const criticalFullSyncIntervalMinutes = Math.max(
|
||||
1,
|
||||
Number.parseInt(
|
||||
process.env.DALPURI_CRITICAL_FULL_SYNC_INTERVAL_MINUTES ?? "60",
|
||||
process.env.DALPURI_CRITICAL_FULL_SYNC_INTERVAL_MINUTES ?? "15",
|
||||
10
|
||||
) || 60
|
||||
) || 15
|
||||
);
|
||||
|
||||
const CRITICAL_FULL_SYNC_INTERVAL_MS =
|
||||
criticalFullSyncIntervalMinutes * 60 * 1000;
|
||||
|
||||
const criticalCwWatermarkOverlapSeconds = Math.max(
|
||||
5,
|
||||
Number.parseInt(
|
||||
process.env.DALPURI_CRITICAL_CW_WATERMARK_OVERLAP_SECONDS ?? "60",
|
||||
10
|
||||
) || 60
|
||||
);
|
||||
|
||||
const CRITICAL_CW_WATERMARK_OVERLAP_MS =
|
||||
criticalCwWatermarkOverlapSeconds * 1000;
|
||||
|
||||
const criticalCwDeltaLimit = Math.max(
|
||||
100,
|
||||
Number.parseInt(process.env.DALPURI_CRITICAL_CW_DELTA_LIMIT ?? "5000", 10) ||
|
||||
5000
|
||||
);
|
||||
|
||||
const lastCriticalFullSyncByStep = new Map<string, number>();
|
||||
const lastCriticalCwWatermarkByStep = new Map<string, Date>();
|
||||
|
||||
const shouldForceCriticalFullSync = (
|
||||
step: Step,
|
||||
@@ -112,6 +149,73 @@ const shouldForceCriticalFullSync = (
|
||||
return true;
|
||||
};
|
||||
|
||||
const computeCriticalCwWatermarkDecision = async (
|
||||
cwPrisma: CwPrismaClient,
|
||||
step: Step,
|
||||
forceIncremental: boolean
|
||||
): Promise<SmartSyncDecision | null> => {
|
||||
if (!forceIncremental) return null;
|
||||
if (!CRITICAL_CW_WATERMARK_TABLES.has(step.name)) return null;
|
||||
|
||||
const cwDelegate = (
|
||||
cwPrisma as unknown as Record<string, { findMany: Function } | undefined>
|
||||
)[step.sourceModel];
|
||||
|
||||
if (!cwDelegate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingWhere =
|
||||
(step.sourceArgs as Record<string, unknown> | undefined)?.where ?? {};
|
||||
|
||||
const lastWatermark = lastCriticalCwWatermarkByStep.get(step.name);
|
||||
const lowerBound = lastWatermark
|
||||
? new Date(lastWatermark.getTime() - CRITICAL_CW_WATERMARK_OVERLAP_MS)
|
||||
: new Date(Date.now() - CRITICAL_CW_WATERMARK_OVERLAP_MS);
|
||||
|
||||
const rows = (await cwDelegate.findMany({
|
||||
select: {
|
||||
[step.sourceIdField]: true,
|
||||
[step.sourceUpdatedField]: true,
|
||||
},
|
||||
where: {
|
||||
...(existingWhere as Record<string, unknown>),
|
||||
[step.sourceUpdatedField]: {
|
||||
gte: lowerBound,
|
||||
},
|
||||
},
|
||||
orderBy: { [step.sourceUpdatedField]: "asc" },
|
||||
take: criticalCwDeltaLimit,
|
||||
})) as Row[];
|
||||
|
||||
if (rows.length >= criticalCwDeltaLimit) {
|
||||
console.warn(
|
||||
` [smart-sync][critical-watermark] ${step.name}: delta reached limit (${criticalCwDeltaLimit}), forcing full sync`
|
||||
);
|
||||
return { mode: "full", differences: [] };
|
||||
}
|
||||
|
||||
if (rows.length > 0) {
|
||||
const latest = rows[rows.length - 1][step.sourceUpdatedField] as Date | null;
|
||||
if (latest) {
|
||||
lastCriticalCwWatermarkByStep.set(step.name, latest);
|
||||
}
|
||||
} else if (!lastWatermark) {
|
||||
lastCriticalCwWatermarkByStep.set(step.name, new Date());
|
||||
}
|
||||
|
||||
const sourceIds = rows.map((r) => r[step.sourceIdField] as number);
|
||||
console.log(
|
||||
` [smart-sync][critical-watermark] ${step.name}: ${sourceIds.length} ids since ${lowerBound.toISOString()}`
|
||||
);
|
||||
|
||||
return {
|
||||
mode: "incremental",
|
||||
sourceIds,
|
||||
differences: [],
|
||||
};
|
||||
};
|
||||
|
||||
const parseEnvFile = (path: string): Record<string, string> => {
|
||||
const envData = readFileSync(path, "utf8");
|
||||
const out: Record<string, string> = {};
|
||||
@@ -253,6 +357,11 @@ const refreshContextFromApi = async (
|
||||
context.scheduleTypeIds.clear();
|
||||
context.scheduleSpanIds.clear();
|
||||
context.taxCodeIds.clear();
|
||||
context.activityTypeIds.clear();
|
||||
context.activityStatusIds.clear();
|
||||
context.activityIds.clear();
|
||||
context.timeEntryChargeCodeIds.clear();
|
||||
context.timeEntryStatusIdByTeStatusId.clear();
|
||||
|
||||
const [
|
||||
users,
|
||||
@@ -270,6 +379,11 @@ const refreshContextFromApi = async (
|
||||
scheduleTypes,
|
||||
scheduleSpans,
|
||||
taxCodes,
|
||||
activityTypes,
|
||||
activityStatuses,
|
||||
activities,
|
||||
timeEntryChargeCodes,
|
||||
timeEntryStatuses,
|
||||
] = await Promise.all([
|
||||
apiPrisma.user.findMany({
|
||||
select: {
|
||||
@@ -349,6 +463,32 @@ const refreshContextFromApi = async (
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.activityType.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.activityStatus.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.activity.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.timeEntryChargeCode.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}),
|
||||
apiPrisma.timeEntryStatus.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
statusId: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const defaultOpportunityType = await apiPrisma.opportunityType.findFirst({
|
||||
@@ -446,6 +586,26 @@ const refreshContextFromApi = async (
|
||||
context.taxCodeIds.add(taxCode.id);
|
||||
}
|
||||
|
||||
for (const activityType of activityTypes) {
|
||||
context.activityTypeIds.add(activityType.id);
|
||||
}
|
||||
|
||||
for (const activityStatus of activityStatuses) {
|
||||
context.activityStatusIds.add(activityStatus.id);
|
||||
}
|
||||
|
||||
for (const activity of activities) {
|
||||
context.activityIds.add(activity.id);
|
||||
}
|
||||
|
||||
for (const chargeCode of timeEntryChargeCodes) {
|
||||
context.timeEntryChargeCodeIds.add(chargeCode.id);
|
||||
}
|
||||
|
||||
for (const status of timeEntryStatuses) {
|
||||
context.timeEntryStatusIdByTeStatusId.set(status.statusId, status.id);
|
||||
}
|
||||
|
||||
context.defaultOpportunityTypeId = defaultOpportunityType?.id ?? null;
|
||||
|
||||
// Optional context fed from env-provided maps.
|
||||
@@ -552,17 +712,17 @@ const sanitizeUserForeignKeys = (
|
||||
targetModel === "serviceTicketNote"
|
||||
? ["authorId"]
|
||||
: targetModel === "serviceTicket"
|
||||
? ["createdById", "updatedById", "closedById"]
|
||||
: [];
|
||||
? ["createdById", "updatedById", "closedById"]
|
||||
: [];
|
||||
|
||||
const identifierFields =
|
||||
targetModel === "serviceTicket"
|
||||
? ["ticketOwnerId"]
|
||||
: targetModel === "company"
|
||||
? ["enteredById", "deletedById"]
|
||||
: targetModel === "companyAddress"
|
||||
? ["updatedById"]
|
||||
: [];
|
||||
? ["enteredById", "deletedById"]
|
||||
: targetModel === "companyAddress"
|
||||
? ["updatedById"]
|
||||
: [];
|
||||
|
||||
for (const field of userIdFields) {
|
||||
const value = sanitized[field];
|
||||
@@ -980,8 +1140,7 @@ const reconcileStepDeletes = async (
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Unknown delete error";
|
||||
console.error(
|
||||
`Delete reconcile failed in ${step.name} for ${
|
||||
step.uniqueField
|
||||
`Delete reconcile failed in ${step.name} for ${step.uniqueField
|
||||
}=${formatValue(value)}:`,
|
||||
message
|
||||
);
|
||||
@@ -992,8 +1151,7 @@ const reconcileStepDeletes = async (
|
||||
|
||||
if (failed > sampleErrorsPrinted) {
|
||||
console.error(
|
||||
`${step.name}: suppressed ${
|
||||
failed - sampleErrorsPrinted
|
||||
`${step.name}: suppressed ${failed - sampleErrorsPrinted
|
||||
} additional delete errors`
|
||||
);
|
||||
}
|
||||
@@ -1004,10 +1162,10 @@ const reconcileStepDeletes = async (
|
||||
type SmartSyncDecision =
|
||||
| { mode: "full"; differences: SmartSyncDifference[] }
|
||||
| {
|
||||
mode: "incremental";
|
||||
sourceIds: number[];
|
||||
differences: SmartSyncDifference[];
|
||||
};
|
||||
mode: "incremental";
|
||||
sourceIds: number[];
|
||||
differences: SmartSyncDifference[];
|
||||
};
|
||||
|
||||
type SmartSyncDifference = {
|
||||
sourceId: number;
|
||||
@@ -1036,10 +1194,8 @@ const logAllSmartSyncDifferences = (
|
||||
? diff.apiUpdatedAt.toISOString()
|
||||
: "null";
|
||||
console.log(
|
||||
` [diff] sourceModel=${step.sourceModel} targetModel=${
|
||||
step.targetModel
|
||||
} id=${diff.sourceId} reason=${
|
||||
diff.reason
|
||||
` [diff] sourceModel=${step.sourceModel} targetModel=${step.targetModel
|
||||
} id=${diff.sourceId} reason=${diff.reason
|
||||
} cwUpdatedAt=${diff.cwUpdatedAt.toISOString()} apiUpdatedAt=${apiUpdated}`
|
||||
);
|
||||
}
|
||||
@@ -1254,8 +1410,8 @@ const syncStep = async (
|
||||
uniqueFields.length > 0
|
||||
? formatUniqueConstraintError(error, translatedData)
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "Unknown row sync error";
|
||||
? error.message
|
||||
: "Unknown row sync error";
|
||||
console.error(
|
||||
`Failed row in ${step.name} (source ${step.sourceModel} -> target ${step.targetModel}):`,
|
||||
message
|
||||
@@ -1300,8 +1456,7 @@ const syncStep = async (
|
||||
|
||||
if (failed > sampleErrorsPrinted) {
|
||||
console.error(
|
||||
`${step.name}: suppressed ${
|
||||
failed - sampleErrorsPrinted
|
||||
`${step.name}: suppressed ${failed - sampleErrorsPrinted
|
||||
} additional row errors`
|
||||
);
|
||||
}
|
||||
@@ -1402,6 +1557,15 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
const isTimedOut = () => Date.now() - syncStartTime > timeoutMs;
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
name: "CW Member Types",
|
||||
sourceModel: "memberType",
|
||||
targetModel: "cwMemberType",
|
||||
translation: cwMemberTypeTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "memberTypeRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
{
|
||||
name: "CW Members",
|
||||
sourceModel: "member",
|
||||
@@ -1506,6 +1670,15 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
sourceIdField: "manufacturerRecId",
|
||||
sourceUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
{
|
||||
name: "Warehouses",
|
||||
sourceModel: "warehouse",
|
||||
targetModel: "warehouse",
|
||||
translation: warehouseTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "warehouseRecId",
|
||||
sourceUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
{
|
||||
name: "Warehouse Bins",
|
||||
sourceModel: "warehouseBin",
|
||||
@@ -1664,6 +1837,11 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
closedFlag: true,
|
||||
},
|
||||
},
|
||||
soInterest: {
|
||||
select: {
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1736,6 +1914,87 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
sourceIdField: "scheduleRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
{
|
||||
name: "Time Entry Statuses",
|
||||
sourceModel: "teStatus",
|
||||
targetModel: "timeEntryStatus",
|
||||
translation: timeEntryStatusTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "teStatusRecId",
|
||||
sourceUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
{
|
||||
name: "Time Entry Charge Codes",
|
||||
sourceModel: "teChargeCode",
|
||||
targetModel: "timeEntryChargeCode",
|
||||
translation: timeEntryChargeCodeTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "teChargeCodeRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
{
|
||||
name: "Time Activity Classes",
|
||||
sourceModel: "activityClass",
|
||||
targetModel: "timeActivityClass",
|
||||
translation: timeActivityClassTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "activityClassRecId",
|
||||
sourceUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
{
|
||||
name: "Time Activity Types",
|
||||
sourceModel: "activityType",
|
||||
targetModel: "timeActivityType",
|
||||
translation: timeActivityTypeTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "activityTypeRecId",
|
||||
sourceUpdatedField: "lastUpdatedUtc",
|
||||
},
|
||||
{
|
||||
name: "Time Entries",
|
||||
sourceModel: "timeEntry",
|
||||
targetModel: "timeEntry",
|
||||
translation: timeEntryTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "timeRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
{
|
||||
name: "Activity Types",
|
||||
sourceModel: "soActivityType",
|
||||
targetModel: "activityType",
|
||||
translation: activityTypeTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "soActivityTypeRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
{
|
||||
name: "Activity Statuses",
|
||||
sourceModel: "soActStatus",
|
||||
targetModel: "activityStatus",
|
||||
translation: activityStatusTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "soActStatusRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
{
|
||||
name: "Activities",
|
||||
sourceModel: "soActivity",
|
||||
targetModel: "activity",
|
||||
translation: activityTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "soActivityRecId",
|
||||
sourceUpdatedField: "lastUpdatedUTC",
|
||||
},
|
||||
{
|
||||
name: "Activity Notes",
|
||||
sourceModel: "soActivityNotes",
|
||||
targetModel: "activityNotes",
|
||||
translation: activityNotesTranslation as unknown as AnyTranslation,
|
||||
uniqueField: "id",
|
||||
sourceIdField: "soActivityNotesRecId",
|
||||
sourceUpdatedField: "lastUpdateUtc",
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -1782,7 +2041,16 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
step.targetModel === "opportunity" ||
|
||||
step.targetModel === "productData" ||
|
||||
step.targetModel === "serviceTicketNote" ||
|
||||
step.targetModel === "schedule"
|
||||
step.targetModel === "schedule" ||
|
||||
step.targetModel === "timeEntryStatus" ||
|
||||
step.targetModel === "timeEntryChargeCode" ||
|
||||
step.targetModel === "timeActivityClass" ||
|
||||
step.targetModel === "timeActivityType" ||
|
||||
step.targetModel === "timeEntry" ||
|
||||
step.targetModel === "activityType" ||
|
||||
step.targetModel === "activityStatus" ||
|
||||
step.targetModel === "activity" ||
|
||||
step.targetModel === "activityNotes"
|
||||
) {
|
||||
try {
|
||||
await refreshContextFromApi(apiPrisma, context);
|
||||
@@ -1808,13 +2076,18 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
step,
|
||||
forceIncremental
|
||||
);
|
||||
const criticalWatermarkDecision = await computeCriticalCwWatermarkDecision(
|
||||
cwPrisma,
|
||||
step,
|
||||
forceIncremental
|
||||
);
|
||||
const forceCriticalFullSync = shouldForceCriticalFullSync(
|
||||
step,
|
||||
forceIncremental
|
||||
);
|
||||
const effectiveDecision = forceCriticalFullSync
|
||||
? ({ mode: "full", differences: decision.differences } as SmartSyncDecision)
|
||||
: decision;
|
||||
: criticalWatermarkDecision ?? decision;
|
||||
|
||||
if (forceCriticalFullSync) {
|
||||
console.log(
|
||||
@@ -1827,12 +2100,10 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
? effectiveDecision.sourceIds
|
||||
: undefined;
|
||||
console.log(
|
||||
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${
|
||||
effectiveDecision.mode
|
||||
}${
|
||||
effectiveDecision.mode === "incremental"
|
||||
? ` (${effectiveDecision.sourceIds.length} ids)`
|
||||
: ""
|
||||
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${effectiveDecision.mode
|
||||
}${effectiveDecision.mode === "incremental"
|
||||
? ` (${effectiveDecision.sourceIds.length} ids)`
|
||||
: ""
|
||||
}`
|
||||
);
|
||||
if (logAllDifferences) {
|
||||
@@ -1856,6 +2127,33 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
`${step.name}: upserted=${result.insertedOrUpdated} skipped=${result.skipped} failed=${result.failed}`
|
||||
);
|
||||
|
||||
// After syncing product inventory, recalculate CatalogItem.onHand for all items
|
||||
// by summing all ProductInventory.qtyOnHand rows grouped by itemId.
|
||||
if (step.targetModel === "productInventory") {
|
||||
console.log(" [post-step] Recalculating CatalogItem.onHand from ProductInventory...");
|
||||
const grouped = await apiPrisma.productInventory.groupBy({
|
||||
by: ["itemId"],
|
||||
_sum: { qtyOnHand: true },
|
||||
where: { itemId: { not: null } },
|
||||
});
|
||||
for (const group of grouped) {
|
||||
if (group.itemId == null) continue;
|
||||
await apiPrisma.catalogItem.updateMany({
|
||||
where: { id: group.itemId },
|
||||
data: { onHand: group._sum.qtyOnHand ?? 0 },
|
||||
});
|
||||
}
|
||||
// Zero out items that have no inventory rows
|
||||
const itemIdsWithInventory = grouped
|
||||
.map((g) => g.itemId)
|
||||
.filter((id): id is number => id != null);
|
||||
await apiPrisma.catalogItem.updateMany({
|
||||
where: { id: { notIn: itemIdsWithInventory } },
|
||||
data: { onHand: 0 },
|
||||
});
|
||||
console.log(` [post-step] Updated onHand for ${grouped.length} catalog items.`);
|
||||
}
|
||||
|
||||
await writeStepLog(
|
||||
step.name,
|
||||
effectiveDecision.mode,
|
||||
@@ -1880,8 +2178,7 @@ export const executeFullDalpuriSync = async (options?: {
|
||||
if (forceIncremental) {
|
||||
const selected = deleteSteps[0];
|
||||
console.log(
|
||||
`[delete-reconcile] incremental sweep: ${selected.name} (${
|
||||
incrementalDeleteStepIndex + 1
|
||||
`[delete-reconcile] incremental sweep: ${selected.name} (${incrementalDeleteStepIndex + 1
|
||||
}/${steps.length})`
|
||||
);
|
||||
incrementalDeleteStepIndex =
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { SoActivityNotes as CwSoActivityNotes } from "../../generated/prisma/client";
|
||||
import { Translation, skipRow } from "./types";
|
||||
import { TranslationContext } from "./context";
|
||||
|
||||
type ApiActivityNotesRecord = {
|
||||
id: number;
|
||||
notes: string;
|
||||
activityId: number | null;
|
||||
internalAnalysisFlag: boolean;
|
||||
enteredById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const activityNotesTranslation: Translation<
|
||||
CwSoActivityNotes,
|
||||
ApiActivityNotesRecord,
|
||||
TranslationContext
|
||||
> = {
|
||||
values: [
|
||||
{ from: "soActivityNotesRecId", to: "id" },
|
||||
{
|
||||
from: "notes",
|
||||
to: "notes",
|
||||
process: (value: string) => value,
|
||||
},
|
||||
{
|
||||
from: "soActivityRecId",
|
||||
to: "activityId",
|
||||
process: (value: number, context: TranslationContext) => {
|
||||
if (!context.activityIds.has(value)) {
|
||||
skipRow(`Activity ${value} not found in API`);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "internalAnalysisFlag",
|
||||
to: "internalAnalysisFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{ from: "enteredBy", to: "enteredById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{ from: "dateCreatedUtc", to: "createdAt" },
|
||||
{ from: "lastUpdateUtc", to: "updatedAt" },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { SoActStatus as CwSoActStatus } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiActivityStatusRecord = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
closedFlag: boolean;
|
||||
inactiveFlag: boolean;
|
||||
defaultFlag: boolean;
|
||||
spawnFollowupFlag: boolean;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const activityStatusTranslation: Translation<
|
||||
CwSoActStatus,
|
||||
ApiActivityStatusRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "soActStatusRecId", to: "id" },
|
||||
{
|
||||
from: "description",
|
||||
to: "name",
|
||||
process: (value) => (value ? value : "Unknown Status"),
|
||||
},
|
||||
{ from: "description", to: "description" },
|
||||
{
|
||||
from: "closedFlag",
|
||||
to: "closedFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "inactiveFlag",
|
||||
to: "inactiveFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "defaultFlag",
|
||||
to: "defaultFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "spawnFollowupFlag",
|
||||
to: "spawnFollowupFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{ from: "dateEnteredUtc", to: "createdAt" },
|
||||
{ from: "lastUpdateUtc", to: "updatedAt" },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { SoActivityType as CwSoActivityType } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiActivityTypeRecord = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
inactiveFlag: boolean;
|
||||
historyFlag: boolean;
|
||||
defaultFlag: boolean;
|
||||
importFlag: boolean;
|
||||
emailFlag: boolean;
|
||||
memoFlag: boolean;
|
||||
pointsValue: number | null;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const activityTypeTranslation: Translation<
|
||||
CwSoActivityType,
|
||||
ApiActivityTypeRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "soActivityTypeRecId", to: "id" },
|
||||
{
|
||||
from: "soActivityTypeId",
|
||||
to: "name",
|
||||
process: (value) => (value ? value : ""),
|
||||
},
|
||||
{
|
||||
from: "description",
|
||||
to: "description",
|
||||
process: (value) => (value ? value : ""),
|
||||
},
|
||||
{
|
||||
from: "inactiveFlag",
|
||||
to: "inactiveFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "historyFlag",
|
||||
to: "historyFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "defaultFlag",
|
||||
to: "defaultFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "importFlag",
|
||||
to: "importFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "emailFlag",
|
||||
to: "emailFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "memoFlag",
|
||||
to: "memoFlag",
|
||||
process: (value) => Boolean(value ?? false),
|
||||
},
|
||||
{ from: "pointsValue", to: "pointsValue" },
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{ from: "dateEnteredUtc", to: "createdAt" },
|
||||
{ from: "lastUpdateUtc", to: "updatedAt" },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { SoActivity as CwSoActivity } from "../../generated/prisma/client";
|
||||
import { Translation, skipRow } from "./types";
|
||||
import { TranslationContext } from "./context";
|
||||
|
||||
type ApiActivityRecord = {
|
||||
id: number;
|
||||
subject: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
assignToId: string;
|
||||
assignedById: string;
|
||||
enteredBy: string;
|
||||
automated: boolean;
|
||||
closedFlag: boolean;
|
||||
notifyCompleteFlag: boolean;
|
||||
notificationSentFlat: boolean;
|
||||
opportunityId: string | null;
|
||||
serviceTicketId: string | null;
|
||||
contactId: number | null;
|
||||
companyId: number | null;
|
||||
activityTypeId: number | null;
|
||||
activityStatusId: number | null;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
closedById: string | null;
|
||||
closedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const activityTranslation: Translation<
|
||||
CwSoActivity,
|
||||
ApiActivityRecord,
|
||||
TranslationContext
|
||||
> = {
|
||||
values: [
|
||||
{ from: "soActivityRecId", to: "id" },
|
||||
{
|
||||
from: "subject",
|
||||
to: "subject",
|
||||
process: (value) => (value ? value : ""),
|
||||
},
|
||||
{
|
||||
from: "dateTimeStart",
|
||||
to: "startTime",
|
||||
process: (value) => {
|
||||
if (!value) skipRow("Missing dateTimeStart");
|
||||
return value as Date;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "dateTimeEnd",
|
||||
to: "endTime",
|
||||
process: (value) => {
|
||||
if (!value) skipRow("Missing dateTimeEnd");
|
||||
return value as Date;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "assignTo",
|
||||
to: "assignToId",
|
||||
},
|
||||
{
|
||||
from: "assignedBy",
|
||||
to: "assignedById",
|
||||
},
|
||||
{
|
||||
from: "enteredBy",
|
||||
to: "enteredBy",
|
||||
},
|
||||
{
|
||||
from: "automated",
|
||||
to: "automated",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "closeFlag",
|
||||
to: "closedFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "notifyCompleteFlag",
|
||||
to: "notifyCompleteFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "notificationSentFlag",
|
||||
to: "notificationSentFlat",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
// opportunityId is a String? field with no @relation — store as null
|
||||
// until a uid-mapping approach is established
|
||||
from: "opportunityRecId",
|
||||
to: "opportunityId",
|
||||
process: () => null,
|
||||
},
|
||||
{
|
||||
// serviceTicketId is a String? field with no @relation — store as null
|
||||
// until a uid-mapping approach is established
|
||||
from: "srServiceRecId",
|
||||
to: "serviceTicketId",
|
||||
process: () => null,
|
||||
},
|
||||
{
|
||||
from: "contactRecId",
|
||||
to: "contactId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.contactIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "companyRecId",
|
||||
to: "companyId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.companyIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "soActivityTypeRecId",
|
||||
to: "activityTypeId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.activityTypeIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "soActStatusRecId",
|
||||
to: "activityStatusId",
|
||||
process: (value: number, context: TranslationContext) => {
|
||||
if (!context.activityStatusIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{ from: "closedBy", to: "closedById" },
|
||||
{ from: "dateClosedUtc", to: "closedAt" },
|
||||
{ from: "dateEnteredUtc", to: "createdAt" },
|
||||
{ from: "lastUpdatedUTC", to: "updatedAt" },
|
||||
],
|
||||
};
|
||||
@@ -45,11 +45,6 @@ export const catalogItemTranslation: Translation<
|
||||
},
|
||||
{ from: "inactiveFlag", to: "inactive" },
|
||||
{ from: "taxableFlag", to: "salesTaxable" },
|
||||
{
|
||||
from: "minimumStock",
|
||||
to: "onHand",
|
||||
process: (value) => (value == null ? 0 : value),
|
||||
},
|
||||
{ from: "classId", to: "classId" },
|
||||
{ from: "dateEnteredUtc", to: "createdAt" },
|
||||
{ from: "lastUpdatedUtc", to: "cwLastUpdated" },
|
||||
|
||||
@@ -46,7 +46,7 @@ export const contactTranslation: Translation<CwContact, ApiContact> = {
|
||||
{
|
||||
from: "lastName",
|
||||
to: "lastName",
|
||||
process: (value) => (value ? value : "Contact"),
|
||||
process: (value) => (value ? value : ""),
|
||||
},
|
||||
{ from: "nickName", to: "nickname" },
|
||||
{ from: "title", to: "title" },
|
||||
|
||||
@@ -73,6 +73,21 @@ export interface TranslationContext {
|
||||
|
||||
// Set of API TaxCode.id values for FK validation
|
||||
taxCodeIds: Set<number>;
|
||||
|
||||
// Set of API ActivityType.id values for FK validation
|
||||
activityTypeIds: Set<number>;
|
||||
|
||||
// Set of API ActivityStatus.id values for FK validation
|
||||
activityStatusIds: Set<number>;
|
||||
|
||||
// Set of API Activity.id values for FK validation (used by ActivityNotes)
|
||||
activityIds: Set<number>;
|
||||
|
||||
// Set of API TimeEntryChargeCode.id values for FK validation
|
||||
timeEntryChargeCodeIds: Set<number>;
|
||||
|
||||
// Map: CW TE_Status_ID (smallint) -> API TimeEntryStatus.id (rec ID)
|
||||
timeEntryStatusIdByTeStatusId: Map<number, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,5 +118,10 @@ export function createTranslationContext(): TranslationContext {
|
||||
scheduleTypeIds: new Set(),
|
||||
scheduleSpanIds: new Set(),
|
||||
taxCodeIds: new Set(),
|
||||
activityTypeIds: new Set(),
|
||||
activityStatusIds: new Set(),
|
||||
activityIds: new Set(),
|
||||
timeEntryChargeCodeIds: new Set(),
|
||||
timeEntryStatusIdByTeStatusId: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MemberType as CwMemberType } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiCwMemberTypeRecord = {
|
||||
id: number;
|
||||
description: string | null;
|
||||
inactiveFlag: boolean;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const cwMemberTypeTranslation: Translation<
|
||||
CwMemberType,
|
||||
ApiCwMemberTypeRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "memberTypeRecId", to: "id" },
|
||||
{ from: "description", to: "description" },
|
||||
{
|
||||
from: "inactiveFlag",
|
||||
to: "inactiveFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{
|
||||
from: "dateEnteredUtc",
|
||||
to: "createdAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
{
|
||||
from: "lastUpdateUtc",
|
||||
to: "updatedAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -47,7 +47,9 @@ type CwOpportunityWithMembers = CwOpportunity & {
|
||||
soOppStatus?: Pick<CwSoOppStatus, "closedFlag"> | null;
|
||||
};
|
||||
|
||||
const toInterest = (value: number | null): OpportunityInterest | null => {
|
||||
const toInterest = (
|
||||
value: number | null
|
||||
): OpportunityInterest | null => {
|
||||
if (value == null) return null;
|
||||
if (value <= 1) return OpportunityInterest.COLD;
|
||||
if (value === 2) return OpportunityInterest.WARM;
|
||||
|
||||
@@ -18,6 +18,7 @@ export const productInventoryTranslation: Translation<
|
||||
to: "createdAt",
|
||||
process: (value) => (value ? value : new Date(0)),
|
||||
},
|
||||
{ from: "warehouseRecId", to: "warehouseId" },
|
||||
{ from: "warehouseBinRecId", to: "warehouseBinId" },
|
||||
{ from: "catalogRecId", to: "itemId" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ActivityClass as CwActivityClass } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiTimeActivityClassRecord = {
|
||||
id: number;
|
||||
description: string | null;
|
||||
hourlyRate: number | null;
|
||||
inactiveFlag: boolean;
|
||||
taxExemptFlag: boolean;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const timeActivityClassTranslation: Translation<
|
||||
CwActivityClass,
|
||||
ApiTimeActivityClassRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "activityClassRecId", to: "id" },
|
||||
{ from: "description", to: "description" },
|
||||
{
|
||||
from: "hourlyRate",
|
||||
to: "hourlyRate",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "inactiveFlag",
|
||||
to: "inactiveFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "taxExemptFlag",
|
||||
to: "taxExemptFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{
|
||||
from: "dateEnteredUtc",
|
||||
to: "createdAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
{
|
||||
from: "lastUpdatedUtc",
|
||||
to: "updatedAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ActivityType as CwActivityType } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiTimeActivityTypeRecord = {
|
||||
id: number;
|
||||
description: string | null;
|
||||
minHours: number | null;
|
||||
maxHours: number | null;
|
||||
rate: number | null;
|
||||
costMultiplier: number;
|
||||
inactiveFlag: boolean;
|
||||
invoiceFlag: boolean;
|
||||
billableFlag: boolean;
|
||||
utilizationFlag: boolean;
|
||||
defaultFlag: boolean;
|
||||
multiplierFlag: boolean;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const timeActivityTypeTranslation: Translation<
|
||||
CwActivityType,
|
||||
ApiTimeActivityTypeRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "activityTypeRecId", to: "id" },
|
||||
{ from: "description", to: "description" },
|
||||
{
|
||||
from: "hoursMin",
|
||||
to: "minHours",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "hoursMax",
|
||||
to: "maxHours",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "rate",
|
||||
to: "rate",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "costMultiplier",
|
||||
to: "costMultiplier",
|
||||
process: (value) => (value != null ? Number(value) : 1),
|
||||
},
|
||||
{
|
||||
from: "inactiveFlag",
|
||||
to: "inactiveFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "invoiceFlag",
|
||||
to: "invoiceFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "billableFlag",
|
||||
to: "billableFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "utilizationFlag",
|
||||
to: "utilizationFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "defaultFlag",
|
||||
to: "defaultFlag",
|
||||
process: (value) => Boolean(value ?? false),
|
||||
},
|
||||
{
|
||||
from: "multiplierFlag",
|
||||
to: "multiplierFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{
|
||||
from: "dateEnteredUtc",
|
||||
to: "createdAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
{
|
||||
from: "lastUpdatedUtc",
|
||||
to: "updatedAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { TeChargeCode as CwTeChargeCode } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiTimeEntryChargeCodeRecord = {
|
||||
id: number;
|
||||
chargeCodeId: number;
|
||||
description: string | null;
|
||||
expenseFlag: boolean;
|
||||
timeFlag: boolean;
|
||||
billableFlag: boolean;
|
||||
invoiceFlag: boolean;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const timeEntryChargeCodeTranslation: Translation<
|
||||
CwTeChargeCode,
|
||||
ApiTimeEntryChargeCodeRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "teChargeCodeRecId", to: "id" },
|
||||
{ from: "teChargeCodeRecId", to: "chargeCodeId" },
|
||||
{ from: "description", to: "description" },
|
||||
{
|
||||
from: "expenseFlag",
|
||||
to: "expenseFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "timeFlag",
|
||||
to: "timeFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "billableFlag",
|
||||
to: "billableFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "invoiceFlag",
|
||||
to: "invoiceFlag",
|
||||
process: (value) => Boolean(value ?? false),
|
||||
},
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{
|
||||
from: "dateEnteredUtc",
|
||||
to: "createdAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
{
|
||||
from: "lastUpdateUtc",
|
||||
to: "updatedAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TeStatus as CwTeStatus } from "../../generated/prisma/client";
|
||||
import { Translation } from "./types";
|
||||
|
||||
type ApiTimeEntryStatusRecord = {
|
||||
id: number;
|
||||
statusId: number;
|
||||
description: string | null;
|
||||
action: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const timeEntryStatusTranslation: Translation<
|
||||
CwTeStatus,
|
||||
ApiTimeEntryStatusRecord
|
||||
> = {
|
||||
values: [
|
||||
{ from: "teStatusRecId", to: "id" },
|
||||
{ from: "teStatusId", to: "statusId" },
|
||||
{ from: "description", to: "description" },
|
||||
{ from: "action", to: "action" },
|
||||
{
|
||||
from: "lastUpdatedUtc",
|
||||
to: "createdAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
{
|
||||
from: "lastUpdatedUtc",
|
||||
to: "updatedAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
import { TimeEntry as CwTimeEntry } from "../../generated/prisma/client";
|
||||
import { Translation, skipRow } from "./types";
|
||||
import { TranslationContext } from "./context";
|
||||
|
||||
type ApiTimeEntryRecord = {
|
||||
id: number;
|
||||
uid: string;
|
||||
memberId: string | null;
|
||||
companyId: number;
|
||||
serviceTicketId: number | null;
|
||||
activityId: number | null;
|
||||
chargeCodeId: number | null;
|
||||
statusId: number | null;
|
||||
contactId: number | null;
|
||||
dateStart: Date | null;
|
||||
timeStart: Date | null;
|
||||
timeEnd: Date | null;
|
||||
notes: string | null;
|
||||
notesMd: string | null;
|
||||
internalNote: string | null;
|
||||
billableHours: number | null;
|
||||
actualHours: number | null;
|
||||
invoicedHours: number | null;
|
||||
deductedHours: number | null;
|
||||
hourlyRate: number | null;
|
||||
effectiveRate: number | null;
|
||||
issueFlag: boolean;
|
||||
mergedFlag: boolean;
|
||||
invoiceFlag: boolean;
|
||||
billableFlag: boolean;
|
||||
documentFlag: boolean;
|
||||
teProblemFlag: boolean;
|
||||
teResolutionFlag: boolean;
|
||||
teInternalAnalysisFlag: boolean;
|
||||
chargeToRecId: number | null;
|
||||
chargeToType: string | null;
|
||||
createdById: string | null;
|
||||
updatedById: string | null;
|
||||
originalAuthorId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export const timeEntryTranslation: Translation<
|
||||
CwTimeEntry,
|
||||
ApiTimeEntryRecord,
|
||||
TranslationContext
|
||||
> = {
|
||||
values: [
|
||||
{ from: "timeRecId", to: "id" },
|
||||
{
|
||||
from: "mobileGuid",
|
||||
to: "uid",
|
||||
process: (value: string) => value,
|
||||
},
|
||||
{ from: "memberId", to: "memberId" },
|
||||
{
|
||||
from: "companyRecId",
|
||||
to: "companyId",
|
||||
process: (value: number, context: TranslationContext) => {
|
||||
if (!context.companyIds.has(value)) return skipRow(`company ${value} not found`);
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "srServiceRecId",
|
||||
to: "serviceTicketId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.serviceTicketIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "soActivityRecId",
|
||||
to: "activityId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.activityIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "teChargeCodeRecId",
|
||||
to: "chargeCodeId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.timeEntryChargeCodeIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "teStatusId",
|
||||
to: "statusId",
|
||||
process: (value: number, context: TranslationContext) => {
|
||||
return context.timeEntryStatusIdByTeStatusId.get(value) ?? null;
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "contactRecId",
|
||||
to: "contactId",
|
||||
process: (value: number | null, context: TranslationContext) => {
|
||||
if (!value) return null;
|
||||
if (!context.contactIds.has(value)) return null;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{ from: "dateStart", to: "dateStart" },
|
||||
{ from: "timeStart", to: "timeStart" },
|
||||
{ from: "timeEnd", to: "timeEnd" },
|
||||
{
|
||||
from: "notes",
|
||||
to: "notes",
|
||||
process: (value: string | null) => value ?? null,
|
||||
},
|
||||
{
|
||||
from: "notesMarkdown",
|
||||
to: "notesMd",
|
||||
process: (value: string | null) => value ?? null,
|
||||
},
|
||||
{ from: "internalNote", to: "internalNote" },
|
||||
{
|
||||
from: "hoursBill",
|
||||
to: "billableHours",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "hoursActual",
|
||||
to: "actualHours",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "hoursInvoiced",
|
||||
to: "invoicedHours",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "hoursDeduct",
|
||||
to: "deductedHours",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "hourlyRate",
|
||||
to: "hourlyRate",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "effectiveRate",
|
||||
to: "effectiveRate",
|
||||
process: (value) => (value != null ? Number(value) : null),
|
||||
},
|
||||
{
|
||||
from: "issueFlag",
|
||||
to: "issueFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "mergedFlag",
|
||||
to: "mergedFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "invoiceFlag",
|
||||
to: "invoiceFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "billableFlag",
|
||||
to: "billableFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "documentFlag",
|
||||
to: "documentFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "teProblemFlag",
|
||||
to: "teProblemFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "teResolutionFlag",
|
||||
to: "teResolutionFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{
|
||||
from: "teInternalAnalysisFlag",
|
||||
to: "teInternalAnalysisFlag",
|
||||
process: (value) => Boolean(value),
|
||||
},
|
||||
{ from: "chargeToRecId", to: "chargeToRecId" },
|
||||
{ from: "chargeToType", to: "chargeToType" },
|
||||
{ from: "enteredBy", to: "createdById" },
|
||||
{ from: "updatedBy", to: "updatedById" },
|
||||
{ from: "originalAuthor", to: "originalAuthorId" },
|
||||
{
|
||||
from: "dateEnteredUtc",
|
||||
to: "createdAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
{
|
||||
from: "lastUpdateUtc",
|
||||
to: "updatedAt",
|
||||
process: (value) => (value as Date | null) ?? new Date(0),
|
||||
},
|
||||
],
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user