Compare commits

...

14 Commits

Author SHA1 Message Date
HoloPanio 212369ff63 feat(migrate): add migration for time entries, activity, and related tables 2026-04-29 02:06:05 +00:00
HoloPanio ffa463ab1a fix: skip electron binary download in UI docker build 2026-04-29 01:52:41 +00:00
HoloPanio 21cba28557 chore: update bun lockfile 2026-04-29 01:24:10 +00:00
HoloPanio fad8143ead test: fix current test suite 2026-04-29 01:13:41 +00:00
HoloPanio db727e0a9d feat: fix and add several things 2026-04-28 01:48:11 +00:00
HoloPanio c3d55b898f chore: make everything current 2026-04-22 01:49:35 +00:00
HoloPanio 6eee7bf0da feat: opportunity detail overhaul, catalog improvements, cleanup
- api: remove stray console.log debug lines across fetch routes, controllers, and workflow dispatch
- api: refactor OpportunityController and cwProbabilityCache
- api: add workflow history endpoint updates
- ui: overhaul opportunity detail page styles and layout
- ui: remove DiscountsTab (consolidated elsewhere), update ProductsTab
- ui: improve catalog page with inventory popover
- ui: update sales API module and server-side page loader
- ui: add sleepy-cat asset, simplify NoResultsMonkey component
2026-04-22 00:53:23 +00:00
HoloPanio 5194d0e21e fix: fixed warehouse inventory numbers and some button verbage 2026-04-21 04:38:07 +00:00
HoloPanio c94de8198f feat(ui): add TTS logo to header with dark/light mode variants 2026-04-21 01:55:07 +00:00
HoloPanio a55850e2c1 feat: add time entry manager, controller, and API routes 2026-04-21 00:52:35 +00:00
HoloPanio 38654601c9 feat: schedule entries, add time modal, and proxy routes
- Add CreateScheduleEntryModal with pill-based type selector and split date/time inputs
- Rewrite AddTimeModal to self-fetch activities with loading/empty states
- Empty state offers 'Create Schedule Entry' shortcut when no open activities
- Add SvelteKit proxy routes for /activities and /time endpoints
- Split datetime-local inputs into separate date+time fields across modals
- Fix CW date format: strip milliseconds from ISO strings (keep Z)
- Add ScheduleEntry to workflow history type whitelist
- Show open schedule entries panel in WorkflowPanel
- Auto-refresh ActivityTab after workflow actions
- Reduce activity dot size and fix connector width overflow
- Hide creation date row for Schedule Entry activities in timeline
2026-04-19 01:26:29 +00:00
HoloPanio 3db045289c fix: trim CW_BASIC_TOKEN and CW_CLIENT_ID to strip trailing whitespace/newlines 2026-04-18 15:47:40 +00:00
HoloPanio cdd9ad64eb fix: update company fetch tests to expect includeAllAddresses 2026-04-18 14:51:31 +00:00
HoloPanio f91d8debcb fix: fix several different data parsing issues 2026-04-18 14:47:06 +00:00
119 changed files with 10526 additions and 1651 deletions
+1
View File
@@ -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.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.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` | 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.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.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` | | `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` |
+50
View File
@@ -212,6 +212,51 @@ export type ScheduleSpan = Prisma.ScheduleSpanModel
* *
*/ */
export type Schedule = Prisma.ScheduleModel 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 * Model CredentialType
* *
@@ -242,3 +287,8 @@ export type TaxCode = Prisma.TaxCodeModel
* *
*/ */
export type CwMember = Prisma.CwMemberModel export type CwMember = Prisma.CwMemberModel
/**
* Model CwMemberType
*
*/
export type CwMemberType = Prisma.CwMemberTypeModel
+50
View File
@@ -236,6 +236,51 @@ export type ScheduleSpan = Prisma.ScheduleSpanModel
* *
*/ */
export type Schedule = Prisma.ScheduleModel 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 * Model CredentialType
* *
@@ -266,3 +311,8 @@ export type TaxCode = Prisma.TaxCodeModel
* *
*/ */
export type CwMember = Prisma.CwMemberModel 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', ScheduleType: 'ScheduleType',
ScheduleSpan: 'ScheduleSpan', ScheduleSpan: 'ScheduleSpan',
Schedule: 'Schedule', Schedule: 'Schedule',
Activity: 'Activity',
ActivityNotes: 'ActivityNotes',
ActivityType: 'ActivityType',
ActivityStatus: 'ActivityStatus',
TimeEntry: 'TimeEntry',
TimeEntryStatus: 'TimeEntryStatus',
TimeEntryChargeCode: 'TimeEntryChargeCode',
TimeActivityClass: 'TimeActivityClass',
TimeActivityType: 'TimeActivityType',
CredentialType: 'CredentialType', CredentialType: 'CredentialType',
SecureValue: 'SecureValue', SecureValue: 'SecureValue',
Credential: 'Credential', Credential: 'Credential',
GeneratedQuotes: 'GeneratedQuotes', GeneratedQuotes: 'GeneratedQuotes',
TaxCode: 'TaxCode', TaxCode: 'TaxCode',
CwMember: 'CwMember' CwMember: 'CwMember',
CwMemberType: 'CwMemberType'
} as const } as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -888,6 +898,204 @@ export const ScheduleScalarFieldEnum = {
export type ScheduleScalarFieldEnum = (typeof ScheduleScalarFieldEnum)[keyof typeof 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 = { export const CredentialTypeScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
@@ -980,6 +1188,20 @@ export const CwMemberScalarFieldEnum = {
export type CwMemberScalarFieldEnum = (typeof CwMemberScalarFieldEnum)[keyof typeof 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 = { export const SortOrder = {
asc: 'asc', asc: 'asc',
desc: 'desc' desc: 'desc'
+10
View File
@@ -47,10 +47,20 @@ export type * from './models/ScheduleStatus.ts'
export type * from './models/ScheduleType.ts' export type * from './models/ScheduleType.ts'
export type * from './models/ScheduleSpan.ts' export type * from './models/ScheduleSpan.ts'
export type * from './models/Schedule.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/CredentialType.ts'
export type * from './models/SecureValue.ts' export type * from './models/SecureValue.ts'
export type * from './models/Credential.ts' export type * from './models/Credential.ts'
export type * from './models/GeneratedQuotes.ts' export type * from './models/GeneratedQuotes.ts'
export type * from './models/TaxCode.ts' export type * from './models/TaxCode.ts'
export type * from './models/CwMember.ts' export type * from './models/CwMember.ts'
export type * from './models/CwMemberType.ts'
export type * from './commonInputTypes.ts' export type * from './commonInputTypes.ts'
+422
View File
@@ -293,6 +293,8 @@ export type CompanyWhereInput = {
credentials?: Prisma.CredentialListRelationFilter credentials?: Prisma.CredentialListRelationFilter
unifiSites?: Prisma.UnifiSiteListRelationFilter unifiSites?: Prisma.UnifiSiteListRelationFilter
opportunities?: Prisma.OpportunityListRelationFilter opportunities?: Prisma.OpportunityListRelationFilter
timeEntries?: Prisma.TimeEntryListRelationFilter
activities?: Prisma.ActivityListRelationFilter
deletedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null deletedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
enteredBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null enteredBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
serviceTickets?: Prisma.ServiceTicketListRelationFilter serviceTickets?: Prisma.ServiceTicketListRelationFilter
@@ -319,6 +321,8 @@ export type CompanyOrderByWithRelationInput = {
credentials?: Prisma.CredentialOrderByRelationAggregateInput credentials?: Prisma.CredentialOrderByRelationAggregateInput
unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput unifiSites?: Prisma.UnifiSiteOrderByRelationAggregateInput
opportunities?: Prisma.OpportunityOrderByRelationAggregateInput opportunities?: Prisma.OpportunityOrderByRelationAggregateInput
timeEntries?: Prisma.TimeEntryOrderByRelationAggregateInput
activities?: Prisma.ActivityOrderByRelationAggregateInput
deletedBy?: Prisma.UserOrderByWithRelationInput deletedBy?: Prisma.UserOrderByWithRelationInput
enteredBy?: Prisma.UserOrderByWithRelationInput enteredBy?: Prisma.UserOrderByWithRelationInput
serviceTickets?: Prisma.ServiceTicketOrderByRelationAggregateInput serviceTickets?: Prisma.ServiceTicketOrderByRelationAggregateInput
@@ -348,6 +352,8 @@ export type CompanyWhereUniqueInput = Prisma.AtLeast<{
credentials?: Prisma.CredentialListRelationFilter credentials?: Prisma.CredentialListRelationFilter
unifiSites?: Prisma.UnifiSiteListRelationFilter unifiSites?: Prisma.UnifiSiteListRelationFilter
opportunities?: Prisma.OpportunityListRelationFilter opportunities?: Prisma.OpportunityListRelationFilter
timeEntries?: Prisma.TimeEntryListRelationFilter
activities?: Prisma.ActivityListRelationFilter
deletedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null deletedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
enteredBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null enteredBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
serviceTickets?: Prisma.ServiceTicketListRelationFilter serviceTickets?: Prisma.ServiceTicketListRelationFilter
@@ -414,6 +420,8 @@ export type CompanyCreateInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -440,6 +448,8 @@ export type CompanyUncheckedCreateInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -462,6 +472,8 @@ export type CompanyUpdateInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -488,6 +500,8 @@ export type CompanyUncheckedUpdateInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -798,6 +812,36 @@ export type CompanyUpdateOneWithoutOpportunitiesNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.CompanyUpdateToOneWithWhereWithoutOpportunitiesInput, Prisma.CompanyUpdateWithoutOpportunitiesInput>, Prisma.CompanyUncheckedUpdateWithoutOpportunitiesInput> 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 = { export type CompanyCreateNestedOneWithoutCredentialsInput = {
create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput> create?: Prisma.XOR<Prisma.CompanyCreateWithoutCredentialsInput, Prisma.CompanyUncheckedCreateWithoutCredentialsInput>
connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput connectOrCreate?: Prisma.CompanyCreateOrConnectWithoutCredentialsInput
@@ -830,6 +874,8 @@ export type CompanyCreateWithoutDeletedByInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
@@ -854,6 +900,8 @@ export type CompanyUncheckedCreateWithoutDeletedByInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -886,6 +934,8 @@ export type CompanyCreateWithoutEnteredByInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
@@ -910,6 +960,8 @@ export type CompanyUncheckedCreateWithoutEnteredByInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -993,6 +1045,8 @@ export type CompanyCreateWithoutUnifiSitesInput = {
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -1018,6 +1072,8 @@ export type CompanyUncheckedCreateWithoutUnifiSitesInput = {
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -1055,6 +1111,8 @@ export type CompanyUpdateWithoutUnifiSitesInput = {
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -1080,6 +1138,8 @@ export type CompanyUncheckedUpdateWithoutUnifiSitesInput = {
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1101,6 +1161,8 @@ export type CompanyCreateWithoutCompanyAddressesInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -1126,6 +1188,8 @@ export type CompanyUncheckedCreateWithoutCompanyAddressesInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -1163,6 +1227,8 @@ export type CompanyUpdateWithoutCompanyAddressesInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -1188,6 +1254,8 @@ export type CompanyUncheckedUpdateWithoutCompanyAddressesInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1209,6 +1277,8 @@ export type CompanyCreateWithoutContactsInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -1234,6 +1304,8 @@ export type CompanyUncheckedCreateWithoutContactsInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -1271,6 +1343,8 @@ export type CompanyUpdateWithoutContactsInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -1296,6 +1370,8 @@ export type CompanyUncheckedUpdateWithoutContactsInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1318,6 +1394,8 @@ export type CompanyCreateWithoutServiceTicketsInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutBillingCompanyInput
@@ -1343,6 +1421,8 @@ export type CompanyUncheckedCreateWithoutServiceTicketsInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -1369,6 +1449,8 @@ export type CompanyCreateWithoutBillingServiceTicketsInput = {
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -1394,6 +1476,8 @@ export type CompanyUncheckedCreateWithoutBillingServiceTicketsInput = {
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
} }
@@ -1431,6 +1515,8 @@ export type CompanyUpdateWithoutServiceTicketsInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
@@ -1456,6 +1542,8 @@ export type CompanyUncheckedUpdateWithoutServiceTicketsInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1488,6 +1576,8 @@ export type CompanyUpdateWithoutBillingServiceTicketsInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -1513,6 +1603,8 @@ export type CompanyUncheckedUpdateWithoutBillingServiceTicketsInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
} }
@@ -1533,6 +1625,8 @@ export type CompanyCreateWithoutOpportunitiesInput = {
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -1558,6 +1652,8 @@ export type CompanyUncheckedCreateWithoutOpportunitiesInput = {
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput credentials?: Prisma.CredentialUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -1595,6 +1691,8 @@ export type CompanyUpdateWithoutOpportunitiesInput = {
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -1620,6 +1718,240 @@ export type CompanyUncheckedUpdateWithoutOpportunitiesInput = {
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput 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 serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1641,6 +1973,8 @@ export type CompanyCreateWithoutCredentialsInput = {
companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput companyAddresses?: Prisma.CompanyAddressCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityCreateNestedManyWithoutCompanyInput
deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput deletedBy?: Prisma.UserCreateNestedOneWithoutCompaniesDeletedInput
enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput enteredBy?: Prisma.UserCreateNestedOneWithoutCompaniesEnteredInput
serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketCreateNestedManyWithoutCompanyInput
@@ -1666,6 +2000,8 @@ export type CompanyUncheckedCreateWithoutCredentialsInput = {
companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput companyAddresses?: Prisma.CompanyAddressUncheckedCreateNestedManyWithoutCompanyInput
unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput unifiSites?: Prisma.UnifiSiteUncheckedCreateNestedManyWithoutCompanyInput
opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput opportunities?: Prisma.OpportunityUncheckedCreateNestedManyWithoutCompanyInput
timeEntries?: Prisma.TimeEntryUncheckedCreateNestedManyWithoutCompanyInput
activities?: Prisma.ActivityUncheckedCreateNestedManyWithoutCompanyInput
serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput serviceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutCompanyInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput billingServiceTickets?: Prisma.ServiceTicketUncheckedCreateNestedManyWithoutBillingCompanyInput
} }
@@ -1703,6 +2039,8 @@ export type CompanyUpdateWithoutCredentialsInput = {
companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput companyAddresses?: Prisma.CompanyAddressUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
@@ -1728,6 +2066,8 @@ export type CompanyUncheckedUpdateWithoutCredentialsInput = {
companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput companyAddresses?: Prisma.CompanyAddressUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1782,6 +2122,8 @@ export type CompanyUpdateWithoutDeletedByInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput enteredBy?: Prisma.UserUpdateOneWithoutCompaniesEnteredNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
@@ -1806,6 +2148,8 @@ export type CompanyUncheckedUpdateWithoutDeletedByInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1844,6 +2188,8 @@ export type CompanyUpdateWithoutEnteredByInput = {
credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUpdateManyWithoutCompanyNestedInput
deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput deletedBy?: Prisma.UserUpdateOneWithoutCompaniesDeletedNestedInput
serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUpdateManyWithoutBillingCompanyNestedInput
@@ -1868,6 +2214,8 @@ export type CompanyUncheckedUpdateWithoutEnteredByInput = {
credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput credentials?: Prisma.CredentialUncheckedUpdateManyWithoutCompanyNestedInput
unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput unifiSites?: Prisma.UnifiSiteUncheckedUpdateManyWithoutCompanyNestedInput
opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput opportunities?: Prisma.OpportunityUncheckedUpdateManyWithoutCompanyNestedInput
timeEntries?: Prisma.TimeEntryUncheckedUpdateManyWithoutCompanyNestedInput
activities?: Prisma.ActivityUncheckedUpdateManyWithoutCompanyNestedInput
serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput serviceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutCompanyNestedInput
billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput billingServiceTickets?: Prisma.ServiceTicketUncheckedUpdateManyWithoutBillingCompanyNestedInput
} }
@@ -1899,6 +2247,8 @@ export type CompanyCountOutputType = {
credentials: number credentials: number
unifiSites: number unifiSites: number
opportunities: number opportunities: number
timeEntries: number
activities: number
serviceTickets: number serviceTickets: number
billingServiceTickets: number billingServiceTickets: number
} }
@@ -1909,6 +2259,8 @@ export type CompanyCountOutputTypeSelect<ExtArgs extends runtime.Types.Extension
credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs credentials?: boolean | CompanyCountOutputTypeCountCredentialsArgs
unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs unifiSites?: boolean | CompanyCountOutputTypeCountUnifiSitesArgs
opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs opportunities?: boolean | CompanyCountOutputTypeCountOpportunitiesArgs
timeEntries?: boolean | CompanyCountOutputTypeCountTimeEntriesArgs
activities?: boolean | CompanyCountOutputTypeCountActivitiesArgs
serviceTickets?: boolean | CompanyCountOutputTypeCountServiceTicketsArgs serviceTickets?: boolean | CompanyCountOutputTypeCountServiceTicketsArgs
billingServiceTickets?: boolean | CompanyCountOutputTypeCountBillingServiceTicketsArgs billingServiceTickets?: boolean | CompanyCountOutputTypeCountBillingServiceTicketsArgs
} }
@@ -1958,6 +2310,20 @@ export type CompanyCountOutputTypeCountOpportunitiesArgs<ExtArgs extends runtime
where?: Prisma.OpportunityWhereInput 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 * CompanyCountOutputType without action
*/ */
@@ -1993,6 +2359,8 @@ export type CompanySelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs> credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs> unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs> opportunities?: boolean | Prisma.Company$opportunitiesArgs<ExtArgs>
timeEntries?: boolean | Prisma.Company$timeEntriesArgs<ExtArgs>
activities?: boolean | Prisma.Company$activitiesArgs<ExtArgs>
deletedBy?: boolean | Prisma.Company$deletedByArgs<ExtArgs> deletedBy?: boolean | Prisma.Company$deletedByArgs<ExtArgs>
enteredBy?: boolean | Prisma.Company$enteredByArgs<ExtArgs> enteredBy?: boolean | Prisma.Company$enteredByArgs<ExtArgs>
serviceTickets?: boolean | Prisma.Company$serviceTicketsArgs<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> credentials?: boolean | Prisma.Company$credentialsArgs<ExtArgs>
unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs> unifiSites?: boolean | Prisma.Company$unifiSitesArgs<ExtArgs>
opportunities?: boolean | Prisma.Company$opportunitiesArgs<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> deletedBy?: boolean | Prisma.Company$deletedByArgs<ExtArgs>
enteredBy?: boolean | Prisma.Company$enteredByArgs<ExtArgs> enteredBy?: boolean | Prisma.Company$enteredByArgs<ExtArgs>
serviceTickets?: boolean | Prisma.Company$serviceTicketsArgs<ExtArgs> serviceTickets?: boolean | Prisma.Company$serviceTicketsArgs<ExtArgs>
@@ -2085,6 +2455,8 @@ export type $CompanyPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
credentials: Prisma.$CredentialPayload<ExtArgs>[] credentials: Prisma.$CredentialPayload<ExtArgs>[]
unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[] unifiSites: Prisma.$UnifiSitePayload<ExtArgs>[]
opportunities: Prisma.$OpportunityPayload<ExtArgs>[] opportunities: Prisma.$OpportunityPayload<ExtArgs>[]
timeEntries: Prisma.$TimeEntryPayload<ExtArgs>[]
activities: Prisma.$ActivityPayload<ExtArgs>[]
deletedBy: Prisma.$UserPayload<ExtArgs> | null deletedBy: Prisma.$UserPayload<ExtArgs> | null
enteredBy: Prisma.$UserPayload<ExtArgs> | null enteredBy: Prisma.$UserPayload<ExtArgs> | null
serviceTickets: Prisma.$ServiceTicketPayload<ExtArgs>[] 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> credentials<T extends Prisma.Company$credentialsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$credentialsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CredentialPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> unifiSites<T extends Prisma.Company$unifiSitesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$unifiSitesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$UnifiSitePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
opportunities<T extends Prisma.Company$opportunitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Company$opportunitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OpportunityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null> 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> 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> 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> 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[] 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 * Company.deletedBy
*/ */
+258
View File
@@ -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
View File
@@ -5,5 +5,6 @@ export default {
}, },
datasource: { datasource: {
url: process.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;
@@ -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;
+332 -44
View File
@@ -124,10 +124,10 @@ enum SyncJobStatus {
} }
model SyncJobRun { model SyncJobRun {
id String @id @default(uuid()) id String @id @default(uuid())
jobType SyncJobType jobType SyncJobType
status SyncJobStatus @default(QUEUED) status SyncJobStatus @default(QUEUED)
triggeredBy String @default("system") triggeredBy String @default("system")
startedAt DateTime? startedAt DateTime?
completedAt DateTime? completedAt DateTime?
@@ -250,6 +250,7 @@ model CorporateLocation {
opportunities Opportunity[] opportunities Opportunity[]
serviceBoards ServiceTicketBoard[] serviceBoards ServiceTicketBoard[]
productDataRecords ProductData[] productDataRecords ProductData[]
timeEntries TimeEntry[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -307,6 +308,9 @@ model Company {
credentials Credential[] credentials Credential[]
unifiSites UnifiSite[] unifiSites UnifiSite[]
opportunities Opportunity[] opportunities Opportunity[]
timeEntries TimeEntry[]
activities Activity[]
deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier]) deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier])
enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier]) enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier])
@@ -384,12 +388,15 @@ model Contact {
memberId Int? memberId Int?
companyId Int? companyId Int?
company Company? @relation(fields: [companyId], references: [id]) company Company? @relation(fields: [companyId], references: [id])
opportunities Opportunity[]
createdAt DateTime @default(now()) opportunities Opportunity[]
updatedAt DateTime @updatedAt
serviceTickets ServiceTicket[] serviceTickets ServiceTicket[]
activities Activity[]
timeEntries TimeEntry[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }
model CatalogItemType { model CatalogItemType {
@@ -606,7 +613,7 @@ model ProductData {
qty Float @default(1) qty Float @default(1)
internalNote String? internalNote String?
shortDescription 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. 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? procurementNotes String?
@@ -692,7 +699,8 @@ model ServiceTicket {
// ------ Billing and invoicing fields ------ // ------ 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? poNumber String?
billCompleteFlag Boolean @default(false) // Bill after ticket is closed, not allowing any billing while open. billCompleteFlag Boolean @default(false) // Bill after ticket is closed, not allowing any billing while open.
@@ -739,12 +747,12 @@ model ServiceTicket {
billingAddressId Int? billingAddressId Int?
severity ServiceTicketSeverity @relation(fields: [severityId], references: [id]) severity ServiceTicketSeverity @relation(fields: [severityId], references: [id])
impact ServiceTicketImpact @relation(fields: [impactId], references: [id]) impact ServiceTicketImpact @relation(fields: [impactId], references: [id])
priority ServiceTicketPriority @relation(fields: [priorityId], references: [id]) priority ServiceTicketPriority @relation(fields: [priorityId], references: [id])
source ServiceTicketSource @relation(fields: [sourceId], references: [id]) source ServiceTicketSource @relation(fields: [sourceId], references: [id])
location ServiceTicketLocation @relation(fields: [locationId], references: [id]) location ServiceTicketLocation @relation(fields: [locationId], references: [id])
serviceTicketBoard ServiceTicketBoard? @relation(fields: [serviceTicketBoardId], references: [id]) serviceTicketBoard ServiceTicketBoard? @relation(fields: [serviceTicketBoardId], references: [id])
ticketOwner User? @relation("ServiceTicketOwner", fields: [ticketOwnerId], references: [cwIdentifier]) ticketOwner User? @relation("ServiceTicketOwner", fields: [ticketOwnerId], references: [cwIdentifier])
company Company? @relation(fields: [companyId], references: [id]) company Company? @relation(fields: [companyId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id]) contact Contact? @relation(fields: [contactId], references: [id])
@@ -1104,9 +1112,9 @@ model ScheduleStatus {
name String name String
description String? // Optima Only field, not synced to CW 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) defaultFlag Boolean @default(false)
schedules Schedule[] schedules Schedule[]
@@ -1122,15 +1130,15 @@ model ScheduleType {
id Int @unique id Int @unique
uid String @id @default(uuid()) uid String @id @default(uuid())
name String name String
description String? // Optima Only field, not synced to CW description String? // Optima Only field, not synced to CW
displayColor String? displayColor String?
tableReference String? tableReference String?
moduleId String? @db.Char(2) moduleId String? @db.Char(2)
scheduleTypeId String? @db.Char(1) scheduleTypeId String? @db.Char(1)
systemFlag Boolean @default(false) systemFlag Boolean @default(false)
displayFlag Boolean @default(false) displayFlag Boolean @default(false)
schedules Schedule[] schedules Schedule[]
@@ -1143,15 +1151,15 @@ model ScheduleType {
} }
model ScheduleSpan { model ScheduleSpan {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
scheduleSpanId String? @db.Char(1) scheduleSpanId String? @db.Char(1)
spanDesc String? @db.Char(20) spanDesc String? @db.Char(20)
schedules Schedule[] schedules Schedule[]
} }
model Schedule { model Schedule {
id Int @unique id Int @unique
uid String @id @default(uuid()) uid String @id @default(uuid())
name String name String
@@ -1159,24 +1167,24 @@ model Schedule {
memberId String? memberId String?
closedFlag Boolean @default(false) closedFlag Boolean @default(false)
reminderFlag Boolean @default(false) reminderFlag Boolean @default(false)
allDayFlag Boolean @default(false) allDayFlag Boolean @default(false)
acknowledgementFlag Boolean @default(false) acknowledgementFlag Boolean @default(false)
meetingFlag Boolean @default(false) meetingFlag Boolean @default(false)
recurringFlag Boolean @default(false) recurringFlag Boolean @default(false)
billableFlag Boolean @default(false) billableFlag Boolean @default(false)
acknowledgedById String?
acknowledgedAt DateTime?
startDate DateTime? acknowledgedById String?
endDate DateTime? acknowledgedAt DateTime?
hoursScheduled Float?
duration Int? // The number of days in between the start and end date. startDate DateTime?
hoursPerDay Float? endDate DateTime?
reminderMinutes Int? @default(15) hoursScheduled Float?
duration Int? // The number of days in between the start and end date.
hoursPerDay Float?
reminderMinutes Int? @default(15)
statusId Int? statusId Int?
status ScheduleStatus? @relation(fields: [statusId], references: [id]) status ScheduleStatus? @relation(fields: [statusId], references: [id])
@@ -1189,9 +1197,274 @@ model Schedule {
updatedById String? updatedById String?
createdById 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()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@@ -1269,12 +1542,12 @@ model GeneratedQuotes {
} }
model TaxCode { model TaxCode {
id Int @unique id Int @unique
uid String @id @default(uuid()) uid String @id @default(uuid())
opportunities Opportunity[] opportunities Opportunity[]
code String @unique code String @unique
codeCaption String codeCaption String
description String? description String?
@@ -1307,3 +1580,18 @@ model CwMember {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt 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
}
@@ -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)
@@ -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)
+2 -2
View File
@@ -18,8 +18,7 @@ export default createRoute(
const includePrimaryContact = const includePrimaryContact =
c.req.query("includePrimaryContact") === "true"; c.req.query("includePrimaryContact") === "true";
const includeAllContacts = c.req.query("includeAllContacts") === "true"; const includeAllContacts = c.req.query("includeAllContacts") === "true";
const includeAllAddresses = c.req.query("includeAllAddresses") === "true";
console.log(company.toJson({ includeAddress, includePrimaryContact, includeAllContacts }));
// Check for address-specific permission if includeAddress is requested // Check for address-specific permission if includeAddress is requested
if (includeAddress) { if (includeAddress) {
@@ -49,6 +48,7 @@ export default createRoute(
includeAddress, includeAddress,
includePrimaryContact, includePrimaryContact,
includeAllContacts, includeAllContacts,
includeAllAddresses,
}); });
const gatedData = await processObjectValuePerms( const gatedData = await processObjectValuePerms(
companyData, companyData,
-2
View File
@@ -35,8 +35,6 @@ export default createRoute(
const data = schema.parse(body); const data = schema.parse(body);
console.log("Creating Credential Type with data:", data);
const credentialType = await credentialTypes.create(data as any); const credentialType = await credentialTypes.create(data as any);
const response = apiResponse.created( const response = apiResponse.created(
@@ -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"] }),
);
+2
View File
@@ -1,6 +1,7 @@
import { default as fetchAll } from "./fetchAll"; import { default as fetchAll } from "./fetchAll";
import { default as fetch } from "./[id]/fetch"; import { default as fetch } from "./[id]/fetch";
import { default as refreshInventory } from "./[id]/refreshInventory"; import { default as refreshInventory } from "./[id]/refreshInventory";
import { default as inventoryByWarehouse } from "./[id]/inventoryByWarehouse";
import { default as link } from "./[id]/link"; import { default as link } from "./[id]/link";
import { default as unlink } from "./[id]/unlink"; import { default as unlink } from "./[id]/unlink";
import { default as fetchLinked } from "./[id]/fetchLinked"; import { default as fetchLinked } from "./[id]/fetchLinked";
@@ -15,6 +16,7 @@ export {
fetchAll, fetchAll,
fetchLinked, fetchLinked,
filters, filters,
inventoryByWarehouse,
link, link,
refreshInventory, refreshInventory,
unlink, unlink,
+7
View File
@@ -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;
+4 -1
View File
@@ -36,7 +36,10 @@ export default createRoute(
if (includes.has("products")) { if (includes.has("products")) {
subResourcePromises.products = item subResourcePromises.products = item
.fetchProducts() .fetchProducts()
.then((products) => products.map((p) => p.toJson())); .then((products) => {
const json = products.map((p) => p.toJson());
return json;
});
} }
if (includes.has("quotes")) { if (includes.has("quotes")) {
subResourcePromises.quotes = generatedQuotes subResourcePromises.quotes = generatedQuotes
+4
View File
@@ -34,6 +34,8 @@ import { default as fetchByUserId } from "./opportunities/fetchByUserId";
import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch"; import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
import { default as workflowStatus } from "./opportunities/[id]/workflow/status"; import { default as workflowStatus } from "./opportunities/[id]/workflow/status";
import { default as workflowHistory } from "./opportunities/[id]/workflow/history"; 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 { export {
addProduct, addProduct,
@@ -72,4 +74,6 @@ export {
workflowDispatch, workflowDispatch,
workflowStatus, workflowStatus,
workflowHistory, 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")) { if (includes.has("products")) {
subResourcePromises.products = item subResourcePromises.products = item
.fetchProducts() .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")) { if (includes.has("quotes")) {
const includeRegenData = c.req.query("includeRegenData") === "true"; const includeRegenData = c.req.query("includeRegenData") === "true";
@@ -7,14 +7,23 @@ import { z } from "zod";
import { cwMembers } from "../../../../../managers/cwMembers"; import { cwMembers } from "../../../../../managers/cwMembers";
import { import {
createWorkflowActivity, createWorkflowActivity,
resolveQuoteParentActivityCwId,
OptimaType, OptimaType,
OpportunityStatus,
} from "../../../../../workflows/wf.opportunity"; } 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 const commitQuoteSchema = z
.object({ .object({
lineItemPricing: z.boolean().optional(), lineItemPricing: z.boolean().optional(),
includeQuoteNarrative: z.boolean().optional(), includeQuoteNarrative: z.boolean().optional(),
includeItemNarratives: z.boolean().optional(), includeItemNarratives: z.boolean().optional(),
separateRecurringServices: z.boolean().optional(),
}) })
.strict() .strict()
.optional(); .optional();
@@ -32,6 +41,28 @@ export default createRoute(
const item = await opportunities.fetchRecord(identifier); const item = await opportunities.fetchRecord(identifier);
const user = c.get("user"); 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); const quote = await item.commitQuote(opts ?? {}, user);
// Create a workflow activity for the generated quote // Create a workflow activity for the generated quote
@@ -44,6 +75,10 @@ export default createRoute(
} }
if (cwMemberId) { if (cwMemberId) {
const parentActivityCwId = await resolveQuoteParentActivityCwId(
item.cwOpportunityId,
);
await createWorkflowActivity({ await createWorkflowActivity({
name: `[Workflow] Quote generated — ${item.name}`, name: `[Workflow] Quote generated — ${item.name}`,
opportunityCwId: item.cwOpportunityId, opportunityCwId: item.cwOpportunityId,
@@ -52,6 +87,7 @@ export default createRoute(
notes: `Quote "${quote.quoteFileName}" generated.`, notes: `Quote "${quote.quoteFileName}" generated.`,
optimaType: OptimaType.QuoteGenerated, optimaType: OptimaType.QuoteGenerated,
quoteId: quote.id, quoteId: quote.id,
parentActivityCwId,
}); });
} }
} catch (activityErr) { } 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"] }),
);
+34 -1
View File
@@ -5,12 +5,18 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../middleware/authorization"; import { authMiddleware } from "../../../middleware/authorization";
import GenericError from "../../../../Errors/GenericError"; import GenericError from "../../../../Errors/GenericError";
import { z } from "zod"; import { z } from "zod";
import {
OpportunityStatus,
StatusIdToKey,
} from "../../../../workflows/wf.opportunity";
import { resolveCwProbabilityId } from "../../../../modules/cw-utils/opportunities/cwProbabilityCache";
const updateSchema = z const updateSchema = z
.object({ .object({
name: z.string().min(1).optional(), name: z.string().min(1).optional(),
notes: z.string().optional(), notes: z.string().optional(),
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(), interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
probability: z.number().min(0).max(100).optional(),
rating: z.object({ id: z.number() }).optional(), rating: z.object({ id: z.number() }).optional(),
type: z.object({ id: z.number() }).optional(), type: z.object({ id: z.number() }).optional(),
stage: 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); 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 { 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( const response = apiResponse.successful(
"Opportunity updated successfully!", "Opportunity updated successfully!",
@@ -91,6 +91,19 @@ const dispatchSchema = z.discriminatedUnion("action", [
action: z.literal("reopen"), action: z.literal("reopen"),
payload: noteRequiredPayload, 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 ───────────────────────────────────────────────────────────────── // ── Route ─────────────────────────────────────────────────────────────────
@@ -103,15 +116,7 @@ export default createRoute(
try { try {
const identifier = c.req.param("identifier"); const identifier = c.req.param("identifier");
const body = await c.req.json(); const body = await c.req.json();
console.log(
"[Workflow Dispatch] Raw request body:",
JSON.stringify(body, null, 2),
);
const parsed = dispatchSchema.parse(body); const parsed = dispatchSchema.parse(body);
console.log(
"[Workflow Dispatch] Parsed payload:",
JSON.stringify(parsed.payload, null, 2),
);
const user = c.get("user"); const user = c.get("user");
// ── Resolve opportunity ──────────────────────────────────────────── // ── Resolve opportunity ────────────────────────────────────────────
@@ -3,9 +3,11 @@ import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status"; import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../../../middleware/authorization"; import { authMiddleware } from "../../../../middleware/authorization";
import { opportunities } from "../../../../../managers/opportunities"; import { opportunities } from "../../../../../managers/opportunities";
import { timeEntries } from "../../../../../managers/timeEntries";
import { activityCw } from "../../../../../modules/cw-utils/activities/activities"; import { activityCw } from "../../../../../modules/cw-utils/activities/activities";
import { ActivityController } from "../../../../../controllers/ActivityController"; import { ActivityController } from "../../../../../controllers/ActivityController";
import { OptimaType } from "../../../../../workflows/wf.opportunity"; import { OptimaType } from "../../../../../workflows/wf.opportunity";
import { prisma } from "../../../../../constants";
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// HELPERS // HELPERS
@@ -22,6 +24,7 @@ const OPTIMA_TYPE_VALUES = new Set<string>([
OptimaType.Revision, OptimaType.Revision,
OptimaType.Finalized, OptimaType.Finalized,
OptimaType.Converted, OptimaType.Converted,
OptimaType.ScheduleEntry,
]); ]);
/** QuoteID custom field ID (matches wf.opportunity.ts QUOTE_ID_FIELD_ID). */ /** 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). */ /** Close Date custom field ID (matches wf.opportunity.ts CLOSE_DATE_FIELD_ID). */
const CLOSE_DATE_FIELD_ID = 49; 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. * Extract the Optima_Type value from a CW activity's custom fields.
* Returns the string value if present, or null. * Returns the string value if present, or null.
@@ -69,6 +75,22 @@ function extractCloseDate(
return field.value; 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 // ROUTE
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -95,6 +117,7 @@ export default createRoute(
activity: ReturnType<ActivityController["toJson"]>; activity: ReturnType<ActivityController["toJson"]>;
optimaType: string; optimaType: string;
quoteId: string | null; quoteId: string | null;
parentActivityId: number | null;
closed: boolean; closed: boolean;
closedAt: string | null; closedAt: string | null;
}[] = []; }[] = [];
@@ -108,6 +131,7 @@ export default createRoute(
if (filterType && optimaType !== filterType) continue; if (filterType && optimaType !== filterType) continue;
const quoteId = extractQuoteId(raw.customFields); const quoteId = extractQuoteId(raw.customFields);
const parentActivityId = extractParentActivityId(raw.customFields);
const closed = raw.status?.id === 2; const closed = raw.status?.id === 2;
const closedAt = extractCloseDate(raw.customFields); const closedAt = extractCloseDate(raw.customFields);
@@ -115,6 +139,7 @@ export default createRoute(
activity: json, activity: json,
optimaType, optimaType,
quoteId, quoteId,
parentActivityId,
closed, closed,
closedAt, closedAt,
}); });
@@ -131,13 +156,60 @@ export default createRoute(
return dateB - dateA; 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( const response = apiResponse.successful(
"Workflow history fetched successfully.", "Workflow history fetched successfully.",
{ {
opportunityId: opportunity.id, opportunityId: opportunity.id,
cwOpportunityId: opportunity.cwOpportunityId, cwOpportunityId: opportunity.cwOpportunityId,
totalActivities: workflowActivities.length, totalActivities: enrichedActivities.length,
activities: workflowActivities, activities: enrichedActivities,
}, },
); );
return c.json(response, response.status as ContentfulStatusCode); return c.json(response, response.status as ContentfulStatusCode);
@@ -248,6 +248,15 @@ const ACTION_MAP: Record<number, AvailableAction[]> = {
requiresPermission: null, requiresPermission: null,
payloadHints: { needsRevision: "true" }, payloadHints: { needsRevision: "true" },
}, },
{
action: "sendBackForRevision",
label: "Send Back for Revision",
targetStatuses: [
{ key: "PendingRevision", id: OpportunityStatus.PendingRevision },
],
requiresNote: true,
requiresPermission: null,
},
], ],
[OpportunityStatus.Active]: [ [OpportunityStatus.Active]: [
+20 -9
View File
@@ -11,6 +11,7 @@ import {
createWorkflowActivity, createWorkflowActivity,
OptimaType, OptimaType,
} from "../../../workflows/wf.opportunity"; } from "../../../workflows/wf.opportunity";
import { resolveCwProbabilityId } from "../../../modules/cw-utils/opportunities/cwProbabilityCache";
const createSchema = z.object({ const createSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
@@ -46,16 +47,25 @@ export default createRoute(
const data = createSchema.parse(body); const data = createSchema.parse(body);
const { interest, ...cwCreateData } = data; const { interest, ...cwCreateData } = data;
try { // Resolve the CW probability reference ID for 50% (the default)
const item = await opportunities.createItem(cwCreateData); const defaultProbabilityId = await resolveCwProbabilityId(50);
if (interest !== undefined) { try {
await prisma.opportunity.update({ const item = await opportunities.createItem({
where: { uid: item.id }, ...cwCreateData,
data: { interest }, ...(defaultProbabilityId != null
}); ? { probability: { id: defaultProbabilityId } }
item.interest = interest; : {}),
} });
// 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 // Create a workflow activity for the new opportunity
try { try {
@@ -118,6 +128,7 @@ export default createRoute(
); );
} }
console.error("[Opportunity Create] DB write failed after CW create:", err);
throw new GenericError({ throw new GenericError({
status: 500, status: 500,
name: "OpportunityCreateError", name: "OpportunityCreateError",
+2
View File
@@ -16,6 +16,7 @@ import procurementRouter from "./routers/procurementRouter";
import salesRouter from "./routers/salesRouter"; import salesRouter from "./routers/salesRouter";
import cwRouter from "./routers/cwRouter"; import cwRouter from "./routers/cwRouter";
import scheduleRouter from "./routers/scheduleRouter"; import scheduleRouter from "./routers/scheduleRouter";
import timeEntryRouter from "./routers/timeEntryRouter";
const app = new Hono(); const app = new Hono();
const v1 = new Hono(); const v1 = new Hono();
@@ -73,6 +74,7 @@ v1.route("/procurement", procurementRouter);
v1.route("/sales", salesRouter); v1.route("/sales", salesRouter);
v1.route("/cw", cwRouter); v1.route("/cw", cwRouter);
v1.route("/schedule", scheduleRouter); v1.route("/schedule", scheduleRouter);
v1.route("/time-entry", timeEntryRouter);
app.route("/v1", v1); app.route("/v1", v1);
export default app; export default app;
@@ -53,6 +53,7 @@ export const registerLiveQuotePreviewHandlers = (socket: Socket) => {
lineItemPricing: opts?.lineItemPricing, lineItemPricing: opts?.lineItemPricing,
includeQuoteNarrative: opts?.includeQuoteNarrative, includeQuoteNarrative: opts?.includeQuoteNarrative,
includeItemNarratives: opts?.includeItemNarratives, includeItemNarratives: opts?.includeItemNarratives,
separateRecurringServices: opts?.separateRecurringServices,
logoPath: opts?.logoPath, logoPath: opts?.logoPath,
showPreview: true, 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"] }),
);
+22
View File
@@ -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"] }),
);
+42
View File
@@ -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"] }),
);
+8
View File
@@ -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"] }),
);
+1 -1
View File
@@ -12,7 +12,7 @@ export default createRoute(
async (c) => { async (c) => {
const siteId = c.req.param("id"); const siteId = c.req.param("id");
const body = await c.req.json(); 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 { companyId } = schema.parse(body);
const site = await unifiSites.linkToCompany(siteId, companyId); const site = await unifiSites.linkToCompany(siteId, companyId);
+2 -2
View File
@@ -74,8 +74,8 @@ export { io, engine };
const connectWiseApi = axios.create({ const connectWiseApi = axios.create({
baseURL: `https://ttscw.totaltech.net/v4_6_release/apis/3.0/`, baseURL: `https://ttscw.totaltech.net/v4_6_release/apis/3.0/`,
headers: { headers: {
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`, Authorization: `Basic ${process.env.CW_BASIC_TOKEN?.trim()}`,
clientId: `${process.env.CW_CLIENT_ID}`, clientId: `${process.env.CW_CLIENT_ID?.trim()}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
timeout: 30_000, // 30 s — prevents indefinite hangs on CW API timeout: 30_000, // 30 s — prevents indefinite hangs on CW API
+5 -2
View File
@@ -5,7 +5,6 @@ import {
CatalogManufacturer, CatalogManufacturer,
} from "../../generated/prisma/client"; } from "../../generated/prisma/client";
import { prisma } from "../constants"; import { prisma } from "../constants";
import { catalogCw } from "../modules/cw-utils/procurement/catalog";
import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types"; import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types";
import GenericError from "../Errors/GenericError"; import GenericError from "../Errors/GenericError";
@@ -129,7 +128,11 @@ export class CatalogItemController {
* @returns {Promise<CatalogItemController>} - The updated controller * @returns {Promise<CatalogItemController>} - The updated controller
*/ */
public async refreshInventory(): Promise<CatalogItemController> { 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) { if (onHand !== this.onHand) {
await prisma.catalogItem.update({ await prisma.catalogItem.update({
+39 -8
View File
@@ -288,19 +288,50 @@ export class CompanyController {
includeAddress?: boolean; includeAddress?: boolean;
includePrimaryContact?: boolean; includePrimaryContact?: boolean;
includeAllContacts?: boolean; includeAllContacts?: boolean;
includeAllAddresses?: boolean;
}) { }) {
const cw_Data: Record<string, unknown> = {}; const cw_Data: Record<string, unknown> = {};
if (opts?.includeAddress && this.cw_Data) { if (opts?.includeAddress) {
const addr = this.cw_Data.company; if (this.cw_Data) {
cw_Data.address = { const addr = this.cw_Data.company;
line1: addr.addressLine1 ?? null, cw_Data.address = {
line2: addr.addressLine2 ?? null, 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, city: addr.city ?? null,
state: addr.state ?? null, state: addr.state ?? null,
zip: addr.zip ?? null, zip: addr.zipCode ?? null,
country: addr.country?.name ?? "United States", country: addr.country ?? null,
}; phone: addr.phone ?? null,
}));
} }
if (opts?.includePrimaryContact) { if (opts?.includePrimaryContact) {
+85 -72
View File
@@ -37,11 +37,11 @@ async function resolveMember(identifier: string | null | undefined) {
}); });
return member return member
? { ? {
id: member.id, id: member.id,
identifier: member.identifier, identifier: member.identifier,
name: `${member.firstName} ${member.lastName}`.trim(), name: `${member.firstName} ${member.lastName}`.trim(),
cwMemberId: member.cwMemberId, cwMemberId: member.cwMemberId,
} }
: { id: null, identifier, name: identifier, cwMemberId: null }; : { id: null, identifier, name: identifier, cwMemberId: null };
} }
@@ -193,8 +193,8 @@ export class OpportunityController {
}); });
const resolvedName = user const resolvedName = user
? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() ||
user.login || user.login ||
user.email user.email
: null; : null;
const name = const name =
resolvedName ?? resolvedName ??
@@ -209,8 +209,8 @@ export class OpportunityController {
constructor( constructor(
data: Opportunity & { data: Opportunity & {
company?: company?:
| (Company & { contacts?: any[]; companyAddresses?: any[] }) | (Company & { contacts?: any[]; companyAddresses?: any[] })
| null; | null;
primarySalesRep?: (User & { roles: Role[] }) | null; primarySalesRep?: (User & { roles: Role[] }) | null;
secondarySalesRep?: (User & { roles: Role[] }) | null; secondarySalesRep?: (User & { roles: Role[] }) | null;
}, },
@@ -222,8 +222,6 @@ export class OpportunityController {
activities?: ActivityController[]; activities?: ActivityController[];
} }
) { ) {
console.log(data.primarySalesRep);
// New schema: uid is the internal PK (string), id is the CW opportunity ID (Int) // New schema: uid is the internal PK (string), id is the CW opportunity ID (Int)
this.id = data.uid; this.id = data.uid;
this.cwOpportunityId = data.id; this.cwOpportunityId = data.id;
@@ -515,8 +513,8 @@ export class OpportunityController {
const hasCwPatch = Object.keys(cwPatch).length > 0; const hasCwPatch = Object.keys(cwPatch).length > 0;
const cwMapped = hasCwPatch const cwMapped = hasCwPatch
? OpportunityController.mapCwToDb( ? OpportunityController.mapCwToDb(
await opportunityCw.update(this.cwOpportunityId, cwPatch) await opportunityCw.update(this.cwOpportunityId, cwPatch)
) )
: {}; : {};
const mapped = const mapped =
@@ -692,10 +690,10 @@ export class OpportunityController {
}, },
company: contact.company company: contact.company
? { ? {
id: contact.company.id, id: contact.company.id,
identifier: null, identifier: null,
name: contact.company.name, name: contact.company.name,
} }
: null, : null,
role: null, role: null,
notes: null, notes: null,
@@ -711,10 +709,10 @@ export class OpportunityController {
contact: ct.contact ? { id: ct.contact.id, name: ct.contact.name } : null, contact: ct.contact ? { id: ct.contact.id, name: ct.contact.name } : null,
company: ct.company company: ct.company
? { ? {
id: ct.company.id, id: ct.company.id,
identifier: ct.company.identifier, identifier: ct.company.identifier,
name: ct.company.name, name: ct.company.name,
} }
: null, : null,
role: ct.role ? { id: ct.role.id, name: ct.role.name } : null, role: ct.role ? { id: ct.role.id, name: ct.role.name } : null,
notes: ct.notes, notes: ct.notes,
@@ -944,6 +942,7 @@ export class OpportunityController {
lineItemPricing?: boolean; lineItemPricing?: boolean;
includeQuoteNarrative?: boolean; includeQuoteNarrative?: boolean;
includeItemNarratives?: boolean; includeItemNarratives?: boolean;
separateRecurringServices?: boolean;
showPreview?: boolean; // INTERNAL ONLY showPreview?: boolean; // INTERNAL ONLY
logoPath?: string; logoPath?: string;
metadata?: QuoteMetadata; metadata?: QuoteMetadata;
@@ -952,6 +951,7 @@ export class OpportunityController {
lineItemPricing: opts?.lineItemPricing ?? true, lineItemPricing: opts?.lineItemPricing ?? true,
includeQuoteNarrative: opts?.includeQuoteNarrative ?? true, includeQuoteNarrative: opts?.includeQuoteNarrative ?? true,
includeItemNarratives: opts?.includeItemNarratives ?? true, includeItemNarratives: opts?.includeItemNarratives ?? true,
separateRecurringServices: opts?.separateRecurringServices ?? true,
showPreview: opts?.showPreview ?? false, showPreview: opts?.showPreview ?? false,
logoPath: opts?.logoPath, logoPath: opts?.logoPath,
}; };
@@ -1016,6 +1016,9 @@ export class OpportunityController {
item.description || item.customerDescription || item.productDescription || "Line Item", item.description || item.customerDescription || item.productDescription || "Line Item",
unitPrice, unitPrice,
narrative: shouldIncludeNarrative ? itemNarrative : undefined, narrative: shouldIncludeNarrative ? itemNarrative : undefined,
isRecurring: options.separateRecurringServices
? item.catalogItemIdentifier?.startsWith("RSV") ?? false
: false,
}; };
}); });
@@ -1056,7 +1059,7 @@ export class OpportunityController {
const quoteNarrativeField = options.includeQuoteNarrative const quoteNarrativeField = options.includeQuoteNarrative
? this._customFields?.find((f) => f.id === 35)?.value?.toString() || ? this._customFields?.find((f) => f.id === 35)?.value?.toString() ||
undefined undefined
: undefined; : undefined;
// Fall back to the customerDescription of a QUO-Narrative product // Fall back to the customerDescription of a QUO-Narrative product
@@ -1068,8 +1071,6 @@ export class OpportunityController {
quoNarrativeProduct?.customerDescription ?? quoNarrativeProduct?.customerDescription ??
undefined; undefined;
console.log("[generateQuote] quoteNarrative:", quoteNarrative);
const companyLine = this.companyName ?? company?.name ?? "Customer Company"; const companyLine = this.companyName ?? company?.name ?? "Customer Company";
// Only show attention if it differs from the customer name // Only show attention if it differs from the customer name
@@ -1122,6 +1123,7 @@ export class OpportunityController {
}, },
isPreview: options.showPreview, isPreview: options.showPreview,
showLineItemPricing: options.lineItemPricing, showLineItemPricing: options.lineItemPricing,
separateRecurringServices: options.separateRecurringServices,
metadata: opts?.metadata, metadata: opts?.metadata,
}; };
@@ -1153,6 +1155,7 @@ export class OpportunityController {
lineItemPricing?: boolean; lineItemPricing?: boolean;
includeQuoteNarrative?: boolean; includeQuoteNarrative?: boolean;
includeItemNarratives?: boolean; includeItemNarratives?: boolean;
separateRecurringServices?: boolean;
logoPath?: string; logoPath?: string;
} = {}, } = {},
user: UserController user: UserController
@@ -1161,6 +1164,7 @@ export class OpportunityController {
lineItemPricing: opts?.lineItemPricing ?? true, lineItemPricing: opts?.lineItemPricing ?? true,
includeQuoteNarrative: opts?.includeQuoteNarrative ?? true, includeQuoteNarrative: opts?.includeQuoteNarrative ?? true,
includeItemNarratives: opts?.includeItemNarratives ?? true, includeItemNarratives: opts?.includeItemNarratives ?? true,
separateRecurringServices: opts?.separateRecurringServices ?? true,
logoPath: opts?.logoPath, logoPath: opts?.logoPath,
}; };
@@ -1242,6 +1246,7 @@ export class OpportunityController {
lineItemPricing: quoteOptions.lineItemPricing, lineItemPricing: quoteOptions.lineItemPricing,
includeQuoteNarrative: quoteOptions.includeQuoteNarrative, includeQuoteNarrative: quoteOptions.includeQuoteNarrative,
includeItemNarratives: quoteOptions.includeItemNarratives, includeItemNarratives: quoteOptions.includeItemNarratives,
separateRecurringServices: quoteOptions.separateRecurringServices,
}, },
// Opportunity metadata // Opportunity metadata
@@ -1264,11 +1269,11 @@ export class OpportunityController {
companyName: this.companyName ?? company?.name ?? null, companyName: this.companyName ?? company?.name ?? null,
primaryContact: companyJson?.cw_Data?.primaryContact primaryContact: companyJson?.cw_Data?.primaryContact
? { ? {
firstName: companyJson.cw_Data.primaryContact.firstName ?? null, firstName: companyJson.cw_Data.primaryContact.firstName ?? null,
lastName: companyJson.cw_Data.primaryContact.lastName ?? null, lastName: companyJson.cw_Data.primaryContact.lastName ?? null,
email: companyJson.cw_Data.primaryContact.email ?? null, email: companyJson.cw_Data.primaryContact.email ?? null,
phone: companyJson.cw_Data.primaryContact.phone ?? null, phone: companyJson.cw_Data.primaryContact.phone ?? null,
} }
: null, : null,
siteAddress: siteAddress.length > 0 ? siteAddress : null, siteAddress: siteAddress.length > 0 ? siteAddress : null,
companyAddress: companyAddress.length > 0 ? companyAddress : null, companyAddress: companyAddress.length > 0 ? companyAddress : null,
@@ -1476,14 +1481,17 @@ export class OpportunityController {
public async resequenceProducts( public async resequenceProducts(
orderedIds: number[] orderedIds: number[]
): Promise<ForecastProductController[]> { ): 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({ const existingRows = await prisma.productData.findMany({
where: { opportunityId: this.cwOpportunityId }, where: { opportunityId: this.cwOpportunityId },
select: { id: true }, select: { id: true },
}); });
const existingIds = new Set(existingRows.map((r) => r.id)); const existingIds = new Set(existingRows.map((r) => r.id));
const sequenceIds = new Set(this.productSequence);
for (const id of orderedIds) { for (const id of orderedIds) {
if (!existingIds.has(id)) { if (!existingIds.has(id) && !sequenceIds.has(id)) {
throw new GenericError({ throw new GenericError({
status: 404, status: 404,
name: "ForecastItemNotFound", name: "ForecastItemNotFound",
@@ -1650,6 +1658,11 @@ export class OpportunityController {
public async deleteProduct(forecastItemId: number): Promise<void> { public async deleteProduct(forecastItemId: number): Promise<void> {
await opportunityCw.deleteProduct(this.cwOpportunityId, forecastItemId); 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 // Remove the deleted item from the local product sequence
if (this.productSequence.includes(forecastItemId)) { if (this.productSequence.includes(forecastItemId)) {
const updatedSequence = this.productSequence.filter( const updatedSequence = this.productSequence.filter(
@@ -1664,7 +1677,7 @@ export class OpportunityController {
this.productSequence = updatedSequence; this.productSequence = updatedSequence;
} }
// No cache invalidation needed return;
} }
/** /**
@@ -1817,13 +1830,13 @@ export class OpportunityController {
const defaultAddr = this._company?.getDefaultAddress(); const defaultAddr = this._company?.getDefaultAddress();
const companyAddress = defaultAddr const companyAddress = defaultAddr
? { ? {
line1: defaultAddr.addressLine1, line1: defaultAddr.addressLine1,
line2: defaultAddr.addressLine2, line2: defaultAddr.addressLine2,
city: defaultAddr.city, city: defaultAddr.city,
state: defaultAddr.state, state: defaultAddr.state,
zip: defaultAddr.zipCode, zip: defaultAddr.zipCode,
country: defaultAddr.country ?? null, country: defaultAddr.country ?? null,
} }
: null; : null;
return { return {
@@ -1850,58 +1863,58 @@ export class OpportunityController {
primarySalesRep: primarySalesRep:
this.primarySalesRepIdentifier || this._primarySalesRep this.primarySalesRepIdentifier || this._primarySalesRep
? { ? {
id: this.primarySalesRepCwId, id: this.primarySalesRepCwId,
identifier: this.primarySalesRepIdentifier, identifier: this.primarySalesRepIdentifier,
name: name:
this._primarySalesRep?.name ?? this._primarySalesRep?.name ??
this.primarySalesRepName ?? this.primarySalesRepName ??
this.primarySalesRepIdentifier, this.primarySalesRepIdentifier,
...(this._primarySalesRep ...(this._primarySalesRep
? { user: this._primarySalesRep.toJson({ safeReturn: true }) } ? { user: this._primarySalesRep.toJson({ safeReturn: true }) }
: {}), : {}),
} }
: null, : null,
secondarySalesRep: secondarySalesRep:
this.secondarySalesRepIdentifier || this._secondarySalesRep this.secondarySalesRepIdentifier || this._secondarySalesRep
? { ? {
id: this.secondarySalesRepCwId, id: this.secondarySalesRepCwId,
identifier: this.secondarySalesRepIdentifier, identifier: this.secondarySalesRepIdentifier,
name: name:
this._secondarySalesRep?.name ?? this._secondarySalesRep?.name ??
this.secondarySalesRepName ?? this.secondarySalesRepName ??
this.secondarySalesRepIdentifier, this.secondarySalesRepIdentifier,
...(this._secondarySalesRep ...(this._secondarySalesRep
? { ? {
user: this._secondarySalesRep.toJson({ user: this._secondarySalesRep.toJson({
safeReturn: true, safeReturn: true,
}), }),
} }
: {}), : {}),
} }
: null, : null,
company: this._company company: this._company
? this._company.toJson({ ? this._company.toJson({
includeAllContacts: true, includeAllContacts: true,
includeAddress: true, includeAddress: true,
includePrimaryContact: false, includePrimaryContact: false,
}) })
: this.companyCwId : this.companyCwId
? { id: this.companyCwId, name: this.companyName } ? { id: this.companyCwId, name: this.companyName }
: null, : null,
contact: this.contactCwId contact: this.contactCwId
? { id: this.contactCwId, name: this.contactName } ? { id: this.contactCwId, name: this.contactName }
: null, : null,
site: this._siteData site: this._siteData
? this._siteData ? this._siteData
: this.siteCwId : this.siteCwId
? { id: this.siteCwId, name: this.siteName } ? { id: this.siteCwId, name: this.siteName }
: null, : null,
customerPO: this.customerPO, customerPO: this.customerPO,
totalSalesTax: this.totalSalesTax, totalSalesTax: this.totalSalesTax,
expectedSalesTaxRate: expectedSalesTaxRate:
this.taxCodeRate !== null ? this.taxCodeRate * 100 : null, this.taxCodeRate !== null ? this.taxCodeRate * 100 : null,
taxCodeDescription: this.taxCodeDescription, taxCodeDescription: this.taxCodeDescription,
probability: this.probability, probability: this.probability != null ? { percent: this.probability } : null,
location: this.locationCwId location: this.locationCwId
? { id: this.locationCwId, name: this.locationName } ? { id: this.locationCwId, name: this.locationName }
: null, : null,
+131
View File
@@ -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,
};
}
}
+96 -3
View File
@@ -46,7 +46,18 @@ export const opportunities = {
// Resolve optional local FKs — nullify any that don't exist locally yet // Resolve optional local FKs — nullify any that don't exist locally yet
// (the sync may be behind; these are all nullable in the schema) // (the sync may be behind; these are all nullable in the schema)
const [companyExists, contactExists, siteExists, typeExists] = await Promise.all([ const [
companyExists,
contactExists,
siteExists,
typeExists,
stageExists,
statusExists,
locationExists,
departmentExists,
primaryRepExists,
secondaryRepExists,
] = await Promise.all([
cwData.company?.id cwData.company?.id
? prisma.company.findFirst({ where: { id: cwData.company.id }, select: { id: true } }) ? prisma.company.findFirst({ where: { id: cwData.company.id }, select: { id: true } })
: null, : null,
@@ -59,21 +70,67 @@ export const opportunities = {
mapped.typeId != null mapped.typeId != null
? prisma.opportunityType.findFirst({ where: { id: mapped.typeId }, select: { id: true } }) ? prisma.opportunityType.findFirst({ where: { id: mapped.typeId }, select: { id: true } })
: null, : 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 companyId = companyExists?.id ?? null;
const contactId = contactExists?.id ?? null; const contactId = contactExists?.id ?? null;
const siteId = siteExists?.id ?? null; const siteId = siteExists?.id ?? null;
const typeId = typeExists?.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({ const record = await prisma.opportunity.create({
data: { data: {
id: cwData.id, id: cwData.id,
...mapped, ...dbFields,
typeId, typeId,
stageId,
statusId,
locationId,
departmentId,
companyId, companyId,
contactId, contactId,
siteId, siteId,
primarySalesRepId,
secondarySalesRepId,
}, },
include: { include: {
company: { include: { contacts: true, companyAddresses: true } }, company: { include: { contacts: true, companyAddresses: true } },
@@ -118,7 +175,7 @@ export const opportunities = {
const record = await prisma.opportunity.findFirst({ const record = await prisma.opportunity.findFirst({
where: isNumeric where: isNumeric
? ({ cwOpportunityId: Number(identifier) } as any) ? { cwOpportunityId: Number(identifier) }
: { uid: identifier as string }, : { uid: identifier as string },
include: { include: {
company: { include: { contacts: true, companyAddresses: true } }, company: { include: { contacts: true, companyAddresses: true } },
@@ -491,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 } }),
]);
},
}; };
+147
View File
@@ -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));
},
};
+2 -2
View File
@@ -66,7 +66,7 @@ export const unifiSites = {
/** /**
* Fetch all UniFi site records linked to a specific company. * 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({ return prisma.unifiSite.findMany({
where: { companyId }, where: { companyId },
}); });
@@ -75,7 +75,7 @@ export const unifiSites = {
/** /**
* Link a UniFi site to a company. * 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 } }); const site = await prisma.unifiSite.findFirst({ where: { id: siteId } });
if (!site) if (!site)
throw new GenericError({ throw new GenericError({
@@ -0,0 +1,63 @@
/**
* cwProbabilityCache
*
* Fetches and caches the list of ConnectWise probability dropdown options.
* Used to resolve a numeric percent (0100) 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, CWOpportunitySummary,
CWForecast, CWForecast,
CWForecastItem, CWForecastItem,
CWOpportunityProduct,
CWForecastItemCreate, CWForecastItemCreate,
CWProcurementProduct, CWProcurementProduct,
CWProcurementProductCreate, CWProcurementProductCreate,
@@ -158,9 +159,9 @@ export const opportunityCw = {
* Fetches the full forecast object (products, revenue summaries, totals) * Fetches the full forecast object (products, revenue summaries, totals)
* for a given opportunity. * for a given opportunity.
*/ */
fetchProducts: async (opportunityId: number): Promise<CWForecast> => { fetchProducts: async (opportunityId: number): Promise<CWOpportunityProduct[]> => {
const response = await connectWiseApi.get( const response = await connectWiseApi.get(
`/sales/opportunities/${opportunityId}/forecast`, `/procurement/products?conditions=opportunity/id=${opportunityId}&pageSize=1000`,
); );
return response.data; return response.data;
}, },
@@ -180,18 +181,18 @@ export const opportunityCw = {
const items_to_add = Array.isArray(data) ? data : [data]; const items_to_add = Array.isArray(data) ? data : [data];
const url = `/sales/opportunities/${opportunityId}/forecast`; 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 existing = await opportunityCw.fetchProducts(opportunityId);
const existingIds = new Set( 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 // Derive sensible defaults from an existing item when available
const templateItem = (existing.forecastItems ?? [])[0]; const templateItem = existing[0];
const defaultStatus = templateItem?.status const defaultStatus = templateItem?.forecastStatus
? { id: templateItem.status.id } ? { id: templateItem.forecastStatus.id }
: { id: 1 }; : { id: 1 };
const defaultForecastType = templateItem?.forecastType ?? "Product"; const defaultForecastType = "Product";
// 2. Build forecast items with required CW fields filled in // 2. Build forecast items with required CW fields filled in
const forecastItems = items_to_add.map((newItem) => ({ const forecastItems = items_to_add.map((newItem) => ({
@@ -234,9 +235,8 @@ export const opportunityCw = {
forecastItemId: number, forecastItemId: number,
data: Record<string, unknown>, data: Record<string, unknown>,
): Promise<CWForecastItem> => { ): Promise<CWForecastItem> => {
const forecast = await opportunityCw.fetchProducts(opportunityId); const items = await opportunityCw.fetchProducts(opportunityId);
const items = forecast.forecastItems ?? []; const idx = items.findIndex((p) => p.forecastDetailId === forecastItemId);
const idx = items.findIndex((fi) => fi.id === forecastItemId);
if (idx === -1) { if (idx === -1) {
throw new Error( throw new Error(
`Forecast item ${forecastItemId} not found on opportunity ${opportunityId}`, `Forecast item ${forecastItemId} not found on opportunity ${opportunityId}`,
@@ -265,14 +265,13 @@ export const opportunityCw = {
opportunityId: number, opportunityId: number,
updates: Map<number, Record<string, unknown>>, updates: Map<number, Record<string, unknown>>,
): Promise<CWForecastItem[]> => { ): Promise<CWForecastItem[]> => {
const forecast = await opportunityCw.fetchProducts(opportunityId); const items = await opportunityCw.fetchProducts(opportunityId);
const items = forecast.forecastItems ?? [];
const operations: { op: "replace"; path: string; value: unknown }[] = []; const operations: { op: "replace"; path: string; value: unknown }[] = [];
const touchedIndices: number[] = []; const touchedIndices: number[] = [];
for (const [itemId, changes] of updates) { 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) { if (idx === -1) {
throw new Error( throw new Error(
`Forecast item ${itemId} not found on opportunity ${opportunityId}`, `Forecast item ${itemId} not found on opportunity ${opportunityId}`,
@@ -304,20 +303,18 @@ export const opportunityCw = {
*/ */
deleteProduct: async ( deleteProduct: async (
opportunityId: number, opportunityId: number,
forecastItemId: number, productId: number,
): Promise<void> => { ): Promise<void> => {
const forecast = await opportunityCw.fetchProducts(opportunityId); const products = await opportunityCw.fetchProducts(opportunityId);
const items = forecast.forecastItems ?? []; const found = products.find((p) => p.id === productId);
if (!found) {
const filtered = items.filter((fi) => fi.id !== forecastItemId);
if (filtered.length === items.length) {
throw new Error( throw new Error(
`Forecast item ${forecastItemId} not found on opportunity ${opportunityId}`, `Product ${productId} not found on opportunity ${opportunityId}`,
); );
} }
const filtered = products.filter((p) => p.id !== productId);
const url = `/sales/opportunities/${opportunityId}/forecast`; const url = `/procurement/products/${productId}`;
await connectWiseApi.put(url, { ...forecast, forecastItems: filtered }); await connectWiseApi.delete(url);
}, },
/** /**
@@ -272,6 +272,7 @@ export interface CWOpportunityUpdate {
stage?: { id: number }; stage?: { id: number };
status?: { id: number }; status?: { id: number };
priority?: { id: number }; priority?: { id: number };
probability?: { id: number };
campaign?: { id: number }; campaign?: { id: number };
primarySalesRep?: { id: number }; primarySalesRep?: { id: number };
secondarySalesRep?: { id: number } | null; secondarySalesRep?: { id: number } | null;
@@ -296,6 +297,7 @@ export interface CWOpportunityCreate {
stage?: { id: number }; stage?: { id: number };
status?: { id: number }; status?: { id: number };
priority?: { id: number }; priority?: { id: number };
probability?: { id: number };
campaign?: { id: number }; campaign?: { id: number };
secondarySalesRep?: { id: number } | null; secondarySalesRep?: { id: number } | null;
site?: { id: number } | null; site?: { id: number } | null;
@@ -311,3 +313,69 @@ export interface CWOpportunitySummary {
id: number; id: number;
_info?: Record<string, string>; _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;
};
}
+215 -98
View File
@@ -7,6 +7,7 @@ export interface QuoteLineItem {
description: string; description: string;
unitPrice: number; unitPrice: number;
narrative?: string; narrative?: string;
isRecurring?: boolean;
} }
export interface CustomerInfo { export interface CustomerInfo {
@@ -60,6 +61,7 @@ export interface QuoteData {
quoteNarrative?: string; quoteNarrative?: string;
isPreview?: boolean; isPreview?: boolean;
showLineItemPricing?: boolean; showLineItemPricing?: boolean;
separateRecurringServices?: boolean;
metadata?: QuoteMetadata; metadata?: QuoteMetadata;
} }
@@ -185,7 +187,16 @@ export async function generateQuote(
logoPath = DEFAULT_LOGO_PATH logoPath = DEFAULT_LOGO_PATH
): Promise<Buffer> { ): Promise<Buffer> {
const t: QuoteTheme = { ...DEFAULT_THEME, ...theme }; 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, (sum, item) => sum + item.qty * item.unitPrice,
0 0
); );
@@ -196,13 +207,23 @@ export async function generateQuote(
const showPricing = data.showLineItemPricing ?? false; 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; const lineTotal = item.qty * item.unitPrice;
return lineTotal < 0 ? sum + lineTotal : sum; return lineTotal < 0 ? sum + lineTotal : sum;
}, 0); }, 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 showDiscount = !showPricing && hasDiscounts;
const recurringTotal = recurringItems.reduce(
(sum, item) => sum + item.qty * item.unitPrice,
0
);
const tableHeader = [ const tableHeader = [
{ text: "Qty", style: "thCell", alignment: "center" }, { text: "Qty", style: "thCell", alignment: "center" },
{ text: "Description", style: "thCell" }, { text: "Description", style: "thCell" },
@@ -218,57 +239,61 @@ export async function generateQuote(
const colCount = showPricing ? 4 : showDiscount ? 3 : 2; const colCount = showPricing ? 4 : showDiscount ? 3 : 2;
const tableRows: Record<string, unknown>[][] = []; function buildTableRows(items: QuoteLineItem[]): Record<string, unknown>[][] {
for (const item of data.lineItems) { const rows: Record<string, unknown>[][] = [];
// Build the description cell — stack description + narrative so they for (const item of items) {
// are a single cell and pdfmake never splits them across pages. const descriptionCell: Record<string, unknown> = item.narrative
const descriptionCell: Record<string, unknown> = item.narrative ? {
? { stack: [
stack: [ { text: item.description, style: "tdCell" },
{ text: item.description, style: "tdCell" }, {
{ text: item.narrative,
text: item.narrative, style: "narrative",
style: "narrative", margin: [0, 2, 8, 0],
margin: [0, 2, 8, 0], },
}, ],
], }
} : { text: item.description, style: "tdCell" };
: { text: item.description, style: "tdCell" };
tableRows.push([ rows.push([
{ text: String(item.qty), style: "tdCell", alignment: "center" }, { text: String(item.qty), style: "tdCell", alignment: "center" },
descriptionCell, descriptionCell,
...(showPricing ...(showPricing
? [ ? [
{ {
text: fmtMoney(item.unitPrice), text: fmtMoney(item.unitPrice),
style: "tdCell", style: "tdCell",
alignment: "right", alignment: "right",
noWrap: true, noWrap: true,
}, },
{ {
text: fmtMoney(item.qty * item.unitPrice), text: fmtMoney(item.qty * item.unitPrice),
style: "tdCell", style: "tdCell",
alignment: "right", alignment: "right",
noWrap: true, noWrap: true,
}, },
] ]
: showDiscount : showDiscount
? [ ? [
{ {
text: text:
item.qty * item.unitPrice < 0 item.qty * item.unitPrice < 0
? fmtMoney(item.qty * item.unitPrice) ? fmtMoney(item.qty * item.unitPrice)
: "", : "",
style: "tdCell", style: "tdCell",
alignment: "right", alignment: "right",
noWrap: true, noWrap: true,
}, },
] ]
: []), : []),
]); ]);
}
return rows;
} }
const tableRows = buildTableRows(regularItems);
const recurringTableRows = buildTableRows(recurringItems);
const headerImage = logoDataUrl const headerImage = logoDataUrl
? { image: logoDataUrl, width: 200 } ? { 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], canvas: [
columns: [
{ {
width: "50%", type: "line",
stack: [ x1: 0,
{ y1: 0,
canvas: [ x2: 220,
{ y2: 0,
type: "line", lineWidth: 0.75,
x1: 0, lineColor: "#999",
y1: 0,
x2: 220,
y2: 0,
lineWidth: 0.75,
lineColor: "#999",
},
],
},
{
text: "Authorized Signature",
fontSize: 7,
color: "#888",
margin: [0, 3, 0, 0],
},
],
}, },
],
},
{
text: "Authorized Signature",
fontSize: 7,
color: "#888",
margin: [0, 3, 0, 0],
},
],
},
{
width: "50%",
stack: [
{
canvas: [
{ {
width: "50%", type: "line",
stack: [ x1: 0,
{ y1: 0,
canvas: [ 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", text: "Monthly Total",
x1: 0, style: "totalFinalLabel",
y1: 0, fillColor: t.headerBg,
x2: 160, margin: [10, 8, 6, 8],
y2: 0, border: [false, false, false, false],
lineWidth: 0.75, },
lineColor: "#999", {
text: fmt(recurringTotal),
style: "totalFinalValue",
alignment: "right",
noWrap: true,
fillColor: t.brandLight,
margin: [6, 7, 8, 7],
border: [false, false, false, false],
}, },
], ],
}, ],
{ },
text: "Date", layout: {
fontSize: 7, hLineWidth: () => 0,
color: "#888", vLineWidth: () => 0,
margin: [0, 3, 0, 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], margin: [0, 0, 0, 0],
stack: [ stack: [
{ {
@@ -827,8 +945,7 @@ export async function generateQuote(
style: "disclaimer", style: "disclaimer",
}, },
], ],
}), });
};
const maybeDoc = printer.createPdfKitDocument(docDefinition as never) as any; const maybeDoc = printer.createPdfKitDocument(docDefinition as never) as any;
const pdfDoc = const pdfDoc =
+7
View File
@@ -568,6 +568,13 @@ export const PERMISSION_NODES = {
usedIn: ["src/api/sales/opportunities/[id]/quotes/commit.ts"], usedIn: ["src/api/sales/opportunities/[id]/quotes/commit.ts"],
dependencies: ["sales.opportunity.fetch"], 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", node: "sales.opportunity.quote.preview",
description: description:
+248 -7
View File
@@ -123,6 +123,7 @@ export const OptimaType = {
Revision: "Revision", Revision: "Revision",
Finalized: "Finalized", Finalized: "Finalized",
Converted: "Converted", Converted: "Converted",
ScheduleEntry: "Schedule Entry",
} as const; } as const;
/** CW custom field ID for the QuoteID field on activities. */ /** 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. */ /** CW custom field ID for the Close Date field on activities. */
const CLOSE_DATE_FIELD_ID = 49; 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 * Optima_Type values whose activities should remain Open until the
* next workflow transition closes them automatically. * next workflow transition closes them automatically.
@@ -139,6 +143,7 @@ const STAYS_OPEN_TYPES = new Set<OptimaTypeValue>([
OptimaType.OpportunitySetup, OptimaType.OpportunitySetup,
OptimaType.OpportunityReview, OptimaType.OpportunityReview,
OptimaType.Revision, OptimaType.Revision,
OptimaType.ScheduleEntry,
]); ]);
export type OptimaTypeValue = export type OptimaTypeValue =
@@ -151,7 +156,8 @@ export type OptimaTypeValue =
| typeof OptimaType.QuoteGenerated | typeof OptimaType.QuoteGenerated
| typeof OptimaType.Revision | typeof OptimaType.Revision
| typeof OptimaType.Finalized | typeof OptimaType.Finalized
| typeof OptimaType.Converted; | typeof OptimaType.Converted
| typeof OptimaType.ScheduleEntry;
/** Permission nodes required by gated transitions. */ /** Permission nodes required by gated transitions. */
export const WorkflowPermissions = { export const WorkflowPermissions = {
@@ -210,6 +216,7 @@ const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
OpportunityStatus.PendingWon, OpportunityStatus.PendingWon,
OpportunityStatus.PendingLost, OpportunityStatus.PendingLost,
OpportunityStatus.Active, OpportunityStatus.Active,
OpportunityStatus.PendingRevision, // needs revision
OpportunityStatus.InternalReview, // cold automation only OpportunityStatus.InternalReview, // cold automation only
]), ]),
@@ -219,6 +226,7 @@ const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
OpportunityStatus.PendingLost, OpportunityStatus.PendingLost,
OpportunityStatus.Active, OpportunityStatus.Active,
OpportunityStatus.InternalReview, // cold automation only OpportunityStatus.InternalReview, // cold automation only
OpportunityStatus.PendingRevision, // send back for revision
]), ]),
[OpportunityStatus.Active]: new Set([ [OpportunityStatus.Active]: new Set([
@@ -317,7 +325,7 @@ export interface SendQuotePayload extends BaseActionPayload {
/** /**
* Quote needs revision. * Quote needs revision.
* Creates a revision activity and transitions to Active. * Creates a revision activity and transitions to PendingRevision.
*/ */
needsRevision?: boolean; needsRevision?: boolean;
} }
@@ -342,6 +350,25 @@ export interface ResurrectPayload extends BaseActionPayload {
/** Begin revision from PendingRevision → Active. */ /** Begin revision from PendingRevision → Active. */
export interface BeginRevisionPayload extends BaseActionPayload {} 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. */ /** Re-send from Active → QuoteSent. */
export interface ResendQuotePayload extends SendQuotePayload {} export interface ResendQuotePayload extends SendQuotePayload {}
@@ -371,7 +398,9 @@ export type WorkflowAction =
| { action: "beginRevision"; payload: BeginRevisionPayload } | { action: "beginRevision"; payload: BeginRevisionPayload }
| { action: "resendQuote"; payload: ResendQuotePayload } | { action: "resendQuote"; payload: ResendQuotePayload }
| { action: "cancel"; payload: CancelPayload } | { action: "cancel"; payload: CancelPayload }
| { action: "reopen"; payload: ReopenPayload }; | { action: "reopen"; payload: ReopenPayload }
| { action: "sendBackForRevision"; payload: SendBackForRevisionPayload }
| { action: "createScheduleEntry"; payload: CreateScheduleEntryPayload };
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Result // Result
@@ -439,11 +468,11 @@ function ok(
/** /**
* Build the `customFields` array for a CW activity with Optima_Type set, * 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( function buildCustomFields(
optimaType: OptimaTypeValue, optimaType: OptimaTypeValue,
opts?: { quoteId?: string; closeDate?: string }, opts?: { quoteId?: string; closeDate?: string; parentActivityCwId?: number },
) { ) {
const fields: any[] = [ 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; return fields;
} }
@@ -494,6 +534,7 @@ export async function createWorkflowActivity(opts: {
quoteId?: string; quoteId?: string;
dateStart?: string; dateStart?: string;
dateEnd?: string; dateEnd?: string;
parentActivityCwId?: number | null;
}): Promise<ActivityController> { }): Promise<ActivityController> {
const shouldStayOpen = STAYS_OPEN_TYPES.has(opts.optimaType); const shouldStayOpen = STAYS_OPEN_TYPES.has(opts.optimaType);
@@ -517,6 +558,7 @@ export async function createWorkflowActivity(opts: {
value: buildCustomFields(opts.optimaType, { value: buildCustomFields(opts.optimaType, {
quoteId: opts.quoteId, quoteId: opts.quoteId,
closeDate: now, closeDate: now,
parentActivityCwId: opts.parentActivityCwId ?? undefined,
}), }),
}, },
]; ];
@@ -533,6 +575,38 @@ export async function createWorkflowActivity(opts: {
return patched; 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. * 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); return ok(currentStatus, targetStatus, activities);
} }
// ── needsRevision flag → Active ──────────────────────────────────── // ── needsRevision flag → Active ──────────────────────────────────────
if (payload.needsRevision) { if (payload.needsRevision) {
const targetStatus = OpportunityStatus.Active; const targetStatus = OpportunityStatus.Active;
@@ -1518,6 +1592,157 @@ export async function beginRevision(
return ok(currentStatus, targetStatus, activities); 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 * Any cancelable status Canceled
* *
@@ -1732,6 +1957,9 @@ async function closeOpenWorkflowActivities(
// Only close activities whose type is in the stays-open set // Only close activities whose type is in the stays-open set
if (!STAYS_OPEN_TYPES.has(optimaField.value as OptimaTypeValue)) continue; 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 closeDate = new Date().toISOString();
const existingFields = (raw.customFields ?? []).map((f: any) => const existingFields = (raw.customFields ?? []).map((f: any) =>
f.id === CLOSE_DATE_FIELD_ID ? { ...f, value: closeDate } : f, 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 ────────── // ── 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 ──────────────────────────────────── // ── Route to transition function ────────────────────────────────────
let result: WorkflowResult; let result: WorkflowResult;
@@ -1856,6 +2087,16 @@ export async function processOpportunityAction(
result = await reopenCancelledOpportunity(opportunity, user, payload); result = await reopenCancelledOpportunity(opportunity, user, payload);
break; 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: { default: {
const _exhaustive: never = action; const _exhaustive: never = action;
return fail(`Unknown workflow action: "${_exhaustive}"`); return fail(`Unknown workflow action: "${_exhaustive}"`);
+258
View File
@@ -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.
+258
View File
@@ -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.
+258
View File
@@ -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.
+14
View File
@@ -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)
+2 -2
View File
@@ -621,7 +621,7 @@
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@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=="], "@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=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "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=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
+376 -110
View File
@@ -666,6 +666,7 @@ model Member {
approvedOpportunities Opportunity[] @relation("OpportunityApprovedBy") approvedOpportunities Opportunity[] @relation("OpportunityApprovedBy")
rejectedOpportunities Opportunity[] @relation("OpportunityRejectedBy") rejectedOpportunities Opportunity[] @relation("OpportunityRejectedBy")
opportunityMembers OpportunityMember[] @relation("OpportunityMemberToMember") opportunityMembers OpportunityMember[] @relation("OpportunityMemberToMember")
memberType MemberType? @relation(fields: [memberTypeRecId], references: [memberTypeRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("Member") @@map("Member")
@@schema("dbo") @@schema("dbo")
@@ -1740,10 +1741,13 @@ model ActivityType {
description String @map("Description") @db.NVarChar(50) description String @map("Description") @db.NVarChar(50)
hoursMin Decimal? @map("Hours_Min") @db.Decimal(18, 2) hoursMin Decimal? @map("Hours_Min") @db.Decimal(18, 2)
hoursMax Decimal? @map("Hours_Max") @db.Decimal(18, 2) hoursMax Decimal? @map("Hours_Max") @db.Decimal(18, 2)
defaultFlag Boolean @map("Default_Flag")
multiplierFlag Boolean @map("Multiplier_Flag") defaultFlag Boolean @map("Default_Flag")
rate Decimal? @map("Rate") @db.Decimal(18, 2) multiplierFlag Boolean @map("Multiplier_Flag")
rateType String? @map("Rate_Type") @db.Char(1)
rate Decimal? @map("Rate") @db.Decimal(18, 2)
rateType String? @map("Rate_Type") @db.Char(1)
inactiveFlag Boolean @map("Inactive_Flag") inactiveFlag Boolean @map("Inactive_Flag")
invoiceFlag Boolean @map("Invoice_Flag") invoiceFlag Boolean @map("Invoice_Flag")
lastUpdate DateTime @map("Last_Update") @db.DateTime2 lastUpdate DateTime @map("Last_Update") @db.DateTime2
@@ -2069,45 +2073,63 @@ model Country {
// ===================== // =====================
model SoActivity { model SoActivity {
soActivityRecId Int @id @map("SO_Activity_Recid") subject String? @map("Subject") @db.NVarChar(100)
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
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") @@map("SO_Activity")
@@schema("dbo") @@schema("dbo")
@@ -2284,83 +2306,84 @@ model ScheduleStatus {
} }
model ScheduleType { model ScheduleType {
scheduleTypeRecId Int @id @map("Schedule_Type_RecID") scheduleTypeRecId Int @id @map("Schedule_Type_RecID")
tableReference String? @map("Table_Reference") @db.NVarChar(50) tableReference String? @map("Table_Reference") @db.NVarChar(50)
description String? @map("Description") @db.NVarChar(50) description String? @map("Description") @db.NVarChar(50)
displayColor String? @map("Display_Color") @db.NVarChar(30) displayColor String? @map("Display_Color") @db.NVarChar(30)
moduleId String? @map("Module_ID") @db.Char(2) moduleId String? @map("Module_ID") @db.Char(2)
systemFlag Boolean @map("System_Flag") systemFlag Boolean @map("System_Flag")
lastUpdate DateTime @map("Last_Update") @db.DateTime2 lastUpdate DateTime @map("Last_Update") @db.DateTime2
updatedBy String? @map("Updated_By") @db.NVarChar(15) updatedBy String? @map("Updated_By") @db.NVarChar(15)
displayFlag Boolean @map("Display_Flag") displayFlag Boolean @map("Display_Flag")
xrefMbrTable String? @map("Xref_Mbr_Table") @db.NVarChar(50) xrefMbrTable String? @map("Xref_Mbr_Table") @db.NVarChar(50)
scheduleTypeId String? @map("Schedule_Type_ID") @db.Char(1) scheduleTypeId String? @map("Schedule_Type_ID") @db.Char(1)
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID") teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
srLocationRecId Int? @map("SR_Location_RecID") srLocationRecId Int? @map("SR_Location_RecID")
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2 lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15) enteredBy String @map("Entered_By") @db.NVarChar(15)
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2 dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
id String @map("Id") @db.UniqueIdentifier id String @map("Id") @db.UniqueIdentifier
schedules Schedule[] schedules Schedule[]
teChargeCode TeChargeCode? @relation(fields: [teChargeCodeRecId], references: [teChargeCodeRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("Schedule_Type") @@map("Schedule_Type")
@@schema("dbo") @@schema("dbo")
} }
model Schedule { model Schedule {
scheduleRecId Int @id @map("Schedule_RecID") scheduleRecId Int @id @map("Schedule_RecID")
recId Int? @map("RecID") recId Int? @map("RecID")
scheduleTypeRecId Int @map("Schedule_Type_RecID") scheduleTypeRecId Int @map("Schedule_Type_RecID")
memberId String? @map("Member_ID") @db.NVarChar(15) memberId String? @map("Member_ID") @db.NVarChar(15)
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
closeFlag Boolean @map("close_flag") closeFlag Boolean @map("close_flag")
hoursEstimated Decimal? @map("Hours_Estimated") @db.Decimal(18, 2) hoursEstimated Decimal? @map("Hours_Estimated") @db.Decimal(18, 2)
lastUpdate DateTime? @map("Last_Update") @db.DateTime lastUpdate DateTime? @map("Last_Update") @db.DateTime
updatedBy String? @map("Updated_By") @db.NVarChar(15) updatedBy String? @map("Updated_By") @db.NVarChar(15)
syncable Boolean @map("Syncable") syncable Boolean @map("Syncable")
lastSync DateTime? @map("Last_Sync") @db.DateTime lastSync DateTime? @map("Last_Sync") @db.DateTime
exchangeGuid String? @map("Exchange_GUID") @db.VarChar(4000) exchangeGuid String? @map("Exchange_GUID") @db.VarChar(4000)
reminderFlag Boolean @map("Reminder_Flag") reminderFlag Boolean @map("Reminder_Flag")
reminderMinutes Int? @map("Reminder_Minutes") reminderMinutes Int? @map("Reminder_Minutes")
allDayFlag Boolean @map("All_Day_Flag") allDayFlag Boolean @map("All_Day_Flag")
duration Int? @map("Duration") duration Int? @map("Duration")
enteredByRecId Int? @map("Entered_By_RecID") enteredByRecId Int? @map("Entered_By_RecID")
xrefMbrRecId Int? @map("Xref_Mbr_RecID") xrefMbrRecId Int? @map("Xref_Mbr_RecID")
percentSched Int? @map("Percent_Sched") percentSched Int? @map("Percent_Sched")
hoursSched Decimal? @map("Hours_Sched") @db.Decimal(18, 2) hoursSched Decimal? @map("Hours_Sched") @db.Decimal(18, 2)
scheduleStatusRecId Int? @map("Schedule_Status_RecID") scheduleStatusRecId Int? @map("Schedule_Status_RecID")
hoursPerDay Decimal? @map("Hours_Per_Day") @db.Decimal(18, 2) hoursPerDay Decimal? @map("Hours_Per_Day") @db.Decimal(18, 2)
ackFlag Boolean? @map("Ack_Flag") ackFlag Boolean? @map("Ack_Flag")
ackMemberRecId Int? @map("Ack_Member_RecID") ackMemberRecId Int? @map("Ack_Member_RecID")
ackDate DateTime? @map("Ack_Date") @db.DateTime ackDate DateTime? @map("Ack_Date") @db.DateTime
closeMemberRecId Int? @map("Close_Member_RecID") closeMemberRecId Int? @map("Close_Member_RecID")
closeDate DateTime? @map("Close_Date") @db.DateTime closeDate DateTime? @map("Close_Date") @db.DateTime
billableFlag Boolean? @map("Billable_Flag") billableFlag Boolean? @map("Billable_Flag")
dateEntered DateTime? @map("Date_Entered") @db.DateTime dateEntered DateTime? @map("Date_Entered") @db.DateTime
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
srLocationRecId Int? @map("SR_Location_RecID") srLocationRecId Int? @map("SR_Location_RecID")
scheduleSpanRecId Int? @map("Schedule_Span_RecID") scheduleSpanRecId Int? @map("Schedule_Span_RecID")
meetingFlag Boolean? @map("Meeting_Flag") meetingFlag Boolean? @map("Meeting_Flag")
recurringFlag Boolean? @map("Recurring_Flag") recurringFlag Boolean? @map("Recurring_Flag")
ackDateUtc DateTime? @map("Ack_Date_UTC") @db.DateTime ackDateUtc DateTime? @map("Ack_Date_UTC") @db.DateTime
dateEnteredUtc DateTime? @map("Date_Entered_UTC") @db.DateTime dateEnteredUtc DateTime? @map("Date_Entered_UTC") @db.DateTime
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
closeDateUtc DateTime? @map("Close_Date_UTC") @db.DateTime closeDateUtc DateTime? @map("Close_Date_UTC") @db.DateTime
enteredBy String? @map("Entered_By") @db.NVarChar(15) enteredBy String? @map("Entered_By") @db.NVarChar(15)
acknowledgedBy String? @map("Acknowledged_By") @db.NVarChar(15) acknowledgedBy String? @map("Acknowledged_By") @db.NVarChar(15)
closedBy String? @map("Closed_By") @db.NVarChar(15) closedBy String? @map("Closed_By") @db.NVarChar(15)
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
scheduleDesc String? @map("Schedule_Desc") @db.NVarChar(250) scheduleDesc String? @map("Schedule_Desc") @db.NVarChar(250)
privateFlag Boolean @map("Private_Flag") privateFlag Boolean @map("Private_Flag")
notifyType String? @map("NotifyType") @db.NVarChar(2) notifyType String? @map("NotifyType") @db.NVarChar(2)
details ScheduleDetail[] details ScheduleDetail[]
status ScheduleStatus? @relation(fields: [scheduleStatusRecId], references: [scheduleStatusRecId], 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) type ScheduleType @relation(fields: [scheduleTypeRecId], references: [scheduleTypeRecId], onDelete: NoAction, onUpdate: NoAction)
span ScheduleSpan? @relation(fields: [scheduleSpanRecId], references: [scheduleSpanRecId], onDelete: NoAction, onUpdate: NoAction) span ScheduleSpan? @relation(fields: [scheduleSpanRecId], references: [scheduleSpanRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("Schedule") @@map("Schedule")
@@schema("dbo") @@schema("dbo")
@@ -2398,3 +2421,246 @@ model ScheduleDetail {
@@map("Schedule_Detail") @@map("Schedule_Detail")
@@schema("dbo") @@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
View File
@@ -11,6 +11,7 @@ export { catalogCategoryTranslation } from "./translations/catalog-category";
export { catalogSubcategoryTranslation } from "./translations/catalog-subcategory"; export { catalogSubcategoryTranslation } from "./translations/catalog-subcategory";
export { catalogManufacturerTranslation } from "./translations/catalog-manufacturer"; export { catalogManufacturerTranslation } from "./translations/catalog-manufacturer";
export { warehouseBinTranslation } from "./translations/warehouse-bin"; export { warehouseBinTranslation } from "./translations/warehouse-bin";
export { warehouseTranslation } from "./translations/warehouse";
export { productInventoryTranslation } from "./translations/product-inventory"; export { productInventoryTranslation } from "./translations/product-inventory";
export { productDataTranslation } from "./translations/product-data.ts"; export { productDataTranslation } from "./translations/product-data.ts";
export { corporateLocationTranslation } from "./translations/corporate-location"; export { corporateLocationTranslation } from "./translations/corporate-location";
@@ -31,6 +32,16 @@ export { scheduleTypeTranslation } from "./translations/schedule-type";
export { scheduleSpanTranslation } from "./translations/schedule-span"; export { scheduleSpanTranslation } from "./translations/schedule-span";
export { scheduleTranslation } from "./translations/schedule"; export { scheduleTranslation } from "./translations/schedule";
export { taxCodeTranslation } from "./translations/tax-code"; 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 // Context type exports
export type { TranslationContext } from "./translations/context"; export type { TranslationContext } from "./translations/context";
+90 -4
View File
@@ -37,8 +37,15 @@ import {
scheduleTypeTranslation, scheduleTypeTranslation,
scheduleSpanTranslation, scheduleSpanTranslation,
scheduleTranslation, scheduleTranslation,
taxCodeTranslation,
timeEntryTranslation,
activityTypeTranslation,
activityStatusTranslation,
activityTranslation,
activityNotesTranslation,
userTranslation, userTranslation,
warehouseBinTranslation, warehouseBinTranslation,
warehouseTranslation,
type TranslationContext, type TranslationContext,
} from "./index"; } from "./index";
import { Translation, SkipRowError } from "./translations/types"; import { Translation, SkipRowError } from "./translations/types";
@@ -203,6 +210,9 @@ const refreshContextFromApi = async (
context.scheduleStatusIds.clear(); context.scheduleStatusIds.clear();
context.scheduleTypeIds.clear(); context.scheduleTypeIds.clear();
context.scheduleSpanIds.clear(); context.scheduleSpanIds.clear();
context.activityTypeIds.clear();
context.activityStatusIds.clear();
context.activityIds.clear();
const [ const [
users, users,
@@ -215,6 +225,9 @@ const refreshContextFromApi = async (
scheduleStatuses, scheduleStatuses,
scheduleTypes, scheduleTypes,
scheduleSpans, scheduleSpans,
activityTypes,
activityStatuses,
activities,
] = await Promise.all([ ] = await Promise.all([
apiPrisma.user.findMany({ apiPrisma.user.findMany({
select: { select: {
@@ -269,6 +282,21 @@ const refreshContextFromApi = async (
id: true, 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({ const defaultOpportunityType = await apiPrisma.opportunityType.findFirst({
@@ -346,6 +374,18 @@ const refreshContextFromApi = async (
context.scheduleSpanIds.add(span.id); 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; context.defaultOpportunityTypeId = defaultOpportunityType?.id ?? null;
// Optional context fed from env-provided maps. // Optional context fed from env-provided maps.
@@ -654,6 +694,13 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
uniqueField: "id", uniqueField: "id",
lastUpdatedField: "lastUpdatedUtc", lastUpdatedField: "lastUpdatedUtc",
}, },
warehouse: {
sourceModel: "warehouse",
targetModel: "warehouse",
translation: warehouseTranslation as unknown as AnyTranslation,
uniqueField: "id",
lastUpdatedField: "lastUpdatedUtc",
},
warehouseBin: { warehouseBin: {
sourceModel: "warehouseBin", sourceModel: "warehouseBin",
targetModel: "warehouseBin", targetModel: "warehouseBin",
@@ -761,6 +808,11 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
closedFlag: true, closedFlag: true,
}, },
}, },
soInterest: {
select: {
description: true,
},
},
}, },
}, },
}, },
@@ -813,6 +865,41 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
uniqueField: "id", uniqueField: "id",
lastUpdatedField: "lastUpdateUtc", 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; return configMap[table] ?? null;
@@ -972,8 +1059,8 @@ export async function syncTableUpdates(
uniqueFields.length > 0 uniqueFields.length > 0
? formatUniqueConstraintError(error, translatedData) ? formatUniqueConstraintError(error, translatedData)
: error instanceof Error : error instanceof Error
? error.message ? error.message
: "Unknown row sync error"; : "Unknown row sync error";
console.error( console.error(
`Failed row in ${config.sourceModel} -> ${config.targetModel}:`, `Failed row in ${config.sourceModel} -> ${config.targetModel}:`,
message message
@@ -984,8 +1071,7 @@ export async function syncTableUpdates(
if (failed > sampleErrorsPrinted) { if (failed > sampleErrorsPrinted) {
console.error( console.error(
`${config.sourceModel}: suppressed ${ `${config.sourceModel}: suppressed ${failed - sampleErrorsPrinted
failed - sampleErrorsPrinted
} additional row errors` } additional row errors`
); );
} }
+231 -32
View File
@@ -39,8 +39,19 @@ import {
scheduleSpanTranslation, scheduleSpanTranslation,
scheduleTranslation, scheduleTranslation,
taxCodeTranslation, taxCodeTranslation,
timeEntryTranslation,
timeEntryStatusTranslation,
timeEntryChargeCodeTranslation,
timeActivityClassTranslation,
timeActivityTypeTranslation,
activityTypeTranslation,
activityStatusTranslation,
activityTranslation,
activityNotesTranslation,
cwMemberTypeTranslation,
userTranslation, userTranslation,
warehouseBinTranslation, warehouseBinTranslation,
warehouseTranslation,
type TranslationContext, type TranslationContext,
} from "./index"; } from "./index";
import { Translation, SkipRowError } from "./translations/types"; import { Translation, SkipRowError } from "./translations/types";
@@ -114,7 +125,7 @@ const CRITICAL_CW_WATERMARK_OVERLAP_MS =
const criticalCwDeltaLimit = Math.max( const criticalCwDeltaLimit = Math.max(
100, 100,
Number.parseInt(process.env.DALPURI_CRITICAL_CW_DELTA_LIMIT ?? "5000", 10) || Number.parseInt(process.env.DALPURI_CRITICAL_CW_DELTA_LIMIT ?? "5000", 10) ||
5000 5000
); );
const lastCriticalFullSyncByStep = new Map<string, number>(); const lastCriticalFullSyncByStep = new Map<string, number>();
@@ -346,6 +357,11 @@ const refreshContextFromApi = async (
context.scheduleTypeIds.clear(); context.scheduleTypeIds.clear();
context.scheduleSpanIds.clear(); context.scheduleSpanIds.clear();
context.taxCodeIds.clear(); context.taxCodeIds.clear();
context.activityTypeIds.clear();
context.activityStatusIds.clear();
context.activityIds.clear();
context.timeEntryChargeCodeIds.clear();
context.timeEntryStatusIdByTeStatusId.clear();
const [ const [
users, users,
@@ -363,6 +379,11 @@ const refreshContextFromApi = async (
scheduleTypes, scheduleTypes,
scheduleSpans, scheduleSpans,
taxCodes, taxCodes,
activityTypes,
activityStatuses,
activities,
timeEntryChargeCodes,
timeEntryStatuses,
] = await Promise.all([ ] = await Promise.all([
apiPrisma.user.findMany({ apiPrisma.user.findMany({
select: { select: {
@@ -442,6 +463,32 @@ const refreshContextFromApi = async (
id: true, 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({ const defaultOpportunityType = await apiPrisma.opportunityType.findFirst({
@@ -539,6 +586,26 @@ const refreshContextFromApi = async (
context.taxCodeIds.add(taxCode.id); 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; context.defaultOpportunityTypeId = defaultOpportunityType?.id ?? null;
// Optional context fed from env-provided maps. // Optional context fed from env-provided maps.
@@ -645,17 +712,17 @@ const sanitizeUserForeignKeys = (
targetModel === "serviceTicketNote" targetModel === "serviceTicketNote"
? ["authorId"] ? ["authorId"]
: targetModel === "serviceTicket" : targetModel === "serviceTicket"
? ["createdById", "updatedById", "closedById"] ? ["createdById", "updatedById", "closedById"]
: []; : [];
const identifierFields = const identifierFields =
targetModel === "serviceTicket" targetModel === "serviceTicket"
? ["ticketOwnerId"] ? ["ticketOwnerId"]
: targetModel === "company" : targetModel === "company"
? ["enteredById", "deletedById"] ? ["enteredById", "deletedById"]
: targetModel === "companyAddress" : targetModel === "companyAddress"
? ["updatedById"] ? ["updatedById"]
: []; : [];
for (const field of userIdFields) { for (const field of userIdFields) {
const value = sanitized[field]; const value = sanitized[field];
@@ -1073,8 +1140,7 @@ const reconcileStepDeletes = async (
const message = const message =
error instanceof Error ? error.message : "Unknown delete error"; error instanceof Error ? error.message : "Unknown delete error";
console.error( console.error(
`Delete reconcile failed in ${step.name} for ${ `Delete reconcile failed in ${step.name} for ${step.uniqueField
step.uniqueField
}=${formatValue(value)}:`, }=${formatValue(value)}:`,
message message
); );
@@ -1085,8 +1151,7 @@ const reconcileStepDeletes = async (
if (failed > sampleErrorsPrinted) { if (failed > sampleErrorsPrinted) {
console.error( console.error(
`${step.name}: suppressed ${ `${step.name}: suppressed ${failed - sampleErrorsPrinted
failed - sampleErrorsPrinted
} additional delete errors` } additional delete errors`
); );
} }
@@ -1097,10 +1162,10 @@ const reconcileStepDeletes = async (
type SmartSyncDecision = type SmartSyncDecision =
| { mode: "full"; differences: SmartSyncDifference[] } | { mode: "full"; differences: SmartSyncDifference[] }
| { | {
mode: "incremental"; mode: "incremental";
sourceIds: number[]; sourceIds: number[];
differences: SmartSyncDifference[]; differences: SmartSyncDifference[];
}; };
type SmartSyncDifference = { type SmartSyncDifference = {
sourceId: number; sourceId: number;
@@ -1129,10 +1194,8 @@ const logAllSmartSyncDifferences = (
? diff.apiUpdatedAt.toISOString() ? diff.apiUpdatedAt.toISOString()
: "null"; : "null";
console.log( console.log(
` [diff] sourceModel=${step.sourceModel} targetModel=${ ` [diff] sourceModel=${step.sourceModel} targetModel=${step.targetModel
step.targetModel } id=${diff.sourceId} reason=${diff.reason
} id=${diff.sourceId} reason=${
diff.reason
} cwUpdatedAt=${diff.cwUpdatedAt.toISOString()} apiUpdatedAt=${apiUpdated}` } cwUpdatedAt=${diff.cwUpdatedAt.toISOString()} apiUpdatedAt=${apiUpdated}`
); );
} }
@@ -1347,8 +1410,8 @@ const syncStep = async (
uniqueFields.length > 0 uniqueFields.length > 0
? formatUniqueConstraintError(error, translatedData) ? formatUniqueConstraintError(error, translatedData)
: error instanceof Error : error instanceof Error
? error.message ? error.message
: "Unknown row sync error"; : "Unknown row sync error";
console.error( console.error(
`Failed row in ${step.name} (source ${step.sourceModel} -> target ${step.targetModel}):`, `Failed row in ${step.name} (source ${step.sourceModel} -> target ${step.targetModel}):`,
message message
@@ -1393,8 +1456,7 @@ const syncStep = async (
if (failed > sampleErrorsPrinted) { if (failed > sampleErrorsPrinted) {
console.error( console.error(
`${step.name}: suppressed ${ `${step.name}: suppressed ${failed - sampleErrorsPrinted
failed - sampleErrorsPrinted
} additional row errors` } additional row errors`
); );
} }
@@ -1495,6 +1557,15 @@ export const executeFullDalpuriSync = async (options?: {
const isTimedOut = () => Date.now() - syncStartTime > timeoutMs; const isTimedOut = () => Date.now() - syncStartTime > timeoutMs;
const steps: Step[] = [ 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", name: "CW Members",
sourceModel: "member", sourceModel: "member",
@@ -1599,6 +1670,15 @@ export const executeFullDalpuriSync = async (options?: {
sourceIdField: "manufacturerRecId", sourceIdField: "manufacturerRecId",
sourceUpdatedField: "lastUpdatedUtc", sourceUpdatedField: "lastUpdatedUtc",
}, },
{
name: "Warehouses",
sourceModel: "warehouse",
targetModel: "warehouse",
translation: warehouseTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "warehouseRecId",
sourceUpdatedField: "lastUpdatedUtc",
},
{ {
name: "Warehouse Bins", name: "Warehouse Bins",
sourceModel: "warehouseBin", sourceModel: "warehouseBin",
@@ -1757,6 +1837,11 @@ export const executeFullDalpuriSync = async (options?: {
closedFlag: true, closedFlag: true,
}, },
}, },
soInterest: {
select: {
description: true,
},
},
}, },
}, },
}, },
@@ -1829,6 +1914,87 @@ export const executeFullDalpuriSync = async (options?: {
sourceIdField: "scheduleRecId", sourceIdField: "scheduleRecId",
sourceUpdatedField: "lastUpdateUtc", 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 { try {
@@ -1875,7 +2041,16 @@ export const executeFullDalpuriSync = async (options?: {
step.targetModel === "opportunity" || step.targetModel === "opportunity" ||
step.targetModel === "productData" || step.targetModel === "productData" ||
step.targetModel === "serviceTicketNote" || 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 { try {
await refreshContextFromApi(apiPrisma, context); await refreshContextFromApi(apiPrisma, context);
@@ -1925,12 +2100,10 @@ export const executeFullDalpuriSync = async (options?: {
? effectiveDecision.sourceIds ? effectiveDecision.sourceIds
: undefined; : undefined;
console.log( console.log(
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${ ` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${effectiveDecision.mode
effectiveDecision.mode }${effectiveDecision.mode === "incremental"
}${ ? ` (${effectiveDecision.sourceIds.length} ids)`
effectiveDecision.mode === "incremental" : ""
? ` (${effectiveDecision.sourceIds.length} ids)`
: ""
}` }`
); );
if (logAllDifferences) { if (logAllDifferences) {
@@ -1954,6 +2127,33 @@ export const executeFullDalpuriSync = async (options?: {
`${step.name}: upserted=${result.insertedOrUpdated} skipped=${result.skipped} failed=${result.failed}` `${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( await writeStepLog(
step.name, step.name,
effectiveDecision.mode, effectiveDecision.mode,
@@ -1978,8 +2178,7 @@ export const executeFullDalpuriSync = async (options?: {
if (forceIncremental) { if (forceIncremental) {
const selected = deleteSteps[0]; const selected = deleteSteps[0];
console.log( console.log(
`[delete-reconcile] incremental sweep: ${selected.name} (${ `[delete-reconcile] incremental sweep: ${selected.name} (${incrementalDeleteStepIndex + 1
incrementalDeleteStepIndex + 1
}/${steps.length})` }/${steps.length})`
); );
incrementalDeleteStepIndex = 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" },
],
};
+73
View File
@@ -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" },
],
};
+147
View File
@@ -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" },
],
};
-5
View File
@@ -45,11 +45,6 @@ export const catalogItemTranslation: Translation<
}, },
{ from: "inactiveFlag", to: "inactive" }, { from: "inactiveFlag", to: "inactive" },
{ from: "taxableFlag", to: "salesTaxable" }, { from: "taxableFlag", to: "salesTaxable" },
{
from: "minimumStock",
to: "onHand",
process: (value) => (value == null ? 0 : value),
},
{ from: "classId", to: "classId" }, { from: "classId", to: "classId" },
{ from: "dateEnteredUtc", to: "createdAt" }, { from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdatedUtc", to: "cwLastUpdated" }, { from: "lastUpdatedUtc", to: "cwLastUpdated" },
+20
View File
@@ -73,6 +73,21 @@ export interface TranslationContext {
// Set of API TaxCode.id values for FK validation // Set of API TaxCode.id values for FK validation
taxCodeIds: Set<number>; 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(), scheduleTypeIds: new Set(),
scheduleSpanIds: new Set(), scheduleSpanIds: new Set(),
taxCodeIds: 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),
},
],
};
+3 -1
View File
@@ -47,7 +47,9 @@ type CwOpportunityWithMembers = CwOpportunity & {
soOppStatus?: Pick<CwSoOppStatus, "closedFlag"> | null; 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 == null) return null;
if (value <= 1) return OpportunityInterest.COLD; if (value <= 1) return OpportunityInterest.COLD;
if (value === 2) return OpportunityInterest.WARM; if (value === 2) return OpportunityInterest.WARM;
@@ -18,6 +18,7 @@ export const productInventoryTranslation: Translation<
to: "createdAt", to: "createdAt",
process: (value) => (value ? value : new Date(0)), process: (value) => (value ? value : new Date(0)),
}, },
{ from: "warehouseRecId", to: "warehouseId" },
{ from: "warehouseBinRecId", to: "warehouseBinId" }, { from: "warehouseBinRecId", to: "warehouseBinId" },
{ from: "catalogRecId", to: "itemId" }, { from: "catalogRecId", to: "itemId" },
{ from: "updatedBy", to: "updatedById" }, { 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),
},
],
};
+208
View File
@@ -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),
},
],
};
@@ -34,6 +34,7 @@ export const warehouseBinTranslation: Translation<
to: "defaultFlag", to: "defaultFlag",
process: (value) => Boolean(value), process: (value) => Boolean(value),
}, },
{ from: "warehouseRecId", to: "warehouseId" },
{ from: "updatedBy", to: "updatedById" }, { from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" }, { from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" }, { from: "lastUpdatedUtc", to: "updatedAt" },
+28
View File
@@ -0,0 +1,28 @@
import { Warehouse as CwWarehouse } from "../../generated/prisma/client";
import { Warehouse as ApiWarehouse } from "../../../api/generated/prisma/client";
import { Translation } from "./types";
export const warehouseTranslation: Translation<CwWarehouse, ApiWarehouse> = {
values: [
{ from: "warehouseRecId", to: "id" },
{
from: "warehouseName",
to: "name",
process: (value) => (value ? value : "Unnamed Warehouse"),
},
{
from: "inactiveFlag",
to: "inactiveFlag",
process: (value) => Boolean(value),
},
{
from: "lockedFlag",
to: "lockedFlag",
process: (value) => Boolean(value),
},
{ from: "updatedBy", to: "updatedById" },
{ from: "enteredBy", to: "createdById" },
{ from: "lastUpdatedUtc", to: "updatedAt" },
{ from: "dateEnteredUtc", to: "createdAt" },
],
};
+1 -1
View File
@@ -8,7 +8,7 @@ COPY patches ./patches
COPY api/package.json ./api/package.json COPY api/package.json ./api/package.json
COPY dalpuri/package.json ./dalpuri/package.json COPY dalpuri/package.json ./dalpuri/package.json
COPY ui/package.json ./ui/package.json COPY ui/package.json ./ui/package.json
RUN bun install --frozen-lockfile RUN ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install --frozen-lockfile
# Copy UI source files # Copy UI source files
COPY ui/ ./ui/ COPY ui/ ./ui/
+4 -19
View File
@@ -14,6 +14,7 @@
type LaborStyle, type LaborStyle,
} from "$lib/optima-api/modules/sales"; } from "$lib/optima-api/modules/sales";
import EditGuard from "./EditGuard.svelte"; import EditGuard from "./EditGuard.svelte";
import InventoryPopover from "./InventoryPopover.svelte";
export let isOpen = false; export let isOpen = false;
export let accessToken: string; export let accessToken: string;
@@ -2669,14 +2670,8 @@
>{formatPrice(item.price)}</span >{formatPrice(item.price)}</span
> >
{/if} {/if}
{#if item.onHand != null && item.onHand > 0} {#if item.onHand != null}
<span class="result-stock in-stock" <InventoryPopover identifier={item.id} onHand={item.onHand} />
>{item.onHand} in stock</span
>
{:else if item.onHand != null}
<span class="result-stock out-of-stock"
>Out of stock</span
>
{/if} {/if}
</div> </div>
{#if cart.some((c) => c.id === item.id)} {#if cart.some((c) => c.id === item.id)}
@@ -3081,17 +3076,7 @@
<div class="detail-field"> <div class="detail-field">
<span class="detail-field-label">Inventory</span> <span class="detail-field-label">Inventory</span>
<span class="detail-field-value"> <span class="detail-field-value">
{#if detailItem.onHand != null && detailItem.onHand > 0} <InventoryPopover identifier={detailItem.id} onHand={detailItem.onHand} />
<span class="stock-badge in-stock"
>{detailItem.onHand} on hand</span
>
{:else if detailItem.onHand != null}
<span class="stock-badge out-of-stock"
>Out of stock</span
>
{:else}
<span class="stock-badge unknown">N/A</span>
{/if}
</span> </span>
</div> </div>
<div class="detail-field"> <div class="detail-field">
+514 -66
View File
@@ -27,6 +27,21 @@
country?: string; country?: string;
} }
interface CompanySite {
id?: number;
uid?: string;
name?: string;
defaultFlag?: boolean;
inactiveFlag?: boolean;
addressLine1?: string | null;
addressLine2?: string | null;
city?: string | null;
state?: string | null;
zip?: string | null;
country?: string | null;
phone?: string | null;
}
export let isOpen = false; export let isOpen = false;
export let onSuccess: () => void = () => {}; export let onSuccess: () => void = () => {};
export let opportunityTypes: OpportunityType[] = []; export let opportunityTypes: OpportunityType[] = [];
@@ -48,6 +63,8 @@
let contacts: CompanyContact[] = []; let contacts: CompanyContact[] = [];
let selectedContactId = ""; let selectedContactId = "";
let companyAddress: CompanyAddress | null = null; let companyAddress: CompanyAddress | null = null;
let allAddresses: CompanySite[] = [];
let selectedSiteUid: string | null = null;
let isLoadingCompanyDetails = false; let isLoadingCompanyDetails = false;
// ── UI state ── // ── UI state ──
@@ -66,10 +83,50 @@
let dropdownStyle = ""; let dropdownStyle = "";
let primaryRepSelect: HTMLSelectElement; let primaryRepSelect: HTMLSelectElement;
// ── Schedule Entry state ──
type ScheduleEntryType = "Follow-Up" | "Appointment" | "Admin";
let scheduleEntryType: ScheduleEntryType = "Follow-Up";
let scheduleEntryStartDate = "";
let scheduleEntryStartHour = "";
let scheduleEntryEndDate = "";
let scheduleEntryEndHour = "";
let scheduleEntryNote = "";
let skipScheduleEntry = false;
let scheduleEntryTimeError = "";
function toCoLocalDate(d: Date): string {
const y = d.getFullYear();
const mo = String(d.getMonth() + 1).padStart(2, "0");
const da = String(d.getDate()).padStart(2, "0");
return `${y}-${mo}-${da}`;
}
function toCoLocalTime(d: Date): string {
const h = String(d.getHours()).padStart(2, "0");
const mi = String(d.getMinutes()).padStart(2, "0");
return `${h}:${mi}`;
}
function initScheduleEntryTimes() {
if (scheduleEntryStartDate && scheduleEntryStartHour && scheduleEntryEndDate && scheduleEntryEndHour) return; // already set
const now = new Date();
const mins = now.getMinutes();
const roundedUp = Math.ceil(mins / 15) * 15;
const ended = new Date(now);
ended.setMinutes(roundedUp, 0, 0);
if (ended <= now) ended.setHours(ended.getHours() + 1);
const started = new Date(ended.getTime() - 60 * 60 * 1000);
scheduleEntryStartDate = toCoLocalDate(started);
scheduleEntryStartHour = toCoLocalTime(started);
scheduleEntryEndDate = toCoLocalDate(ended);
scheduleEntryEndHour = toCoLocalTime(ended);
}
// ── Opportunity types state ── // ── Opportunity types state ──
let loadedTypes: OpportunityType[] = []; let loadedTypes: OpportunityType[] = [];
let isLoadingTypes = false; let isLoadingTypes = false;
$: resolvedTypes = opportunityTypes.length > 0 ? opportunityTypes : loadedTypes; $: resolvedTypes = opportunityTypes.length > 0 ? opportunityTypes : loadedTypes;
$: selectedTypeName = resolvedTypes.find((t) => String(t.id) === typeId)?.name ?? null;
async function loadOpportunityTypes() { async function loadOpportunityTypes() {
if (opportunityTypes.length > 0) return; if (opportunityTypes.length > 0) return;
@@ -93,14 +150,16 @@
{ label: "Details", icon: "document" }, { label: "Details", icon: "document" },
{ label: "Assignment", icon: "people" }, { label: "Assignment", icon: "people" },
{ label: "Contact & Site", icon: "contact" }, { label: "Contact & Site", icon: "contact" },
{ label: "Schedule Entry", icon: "calendar" },
{ label: "Review", icon: "check" }, { label: "Review", icon: "check" },
]; ];
// ── Validation ── // ── Validation ──
$: isStep0Valid = name.trim().length > 0 && expectedCloseDate.length > 0; $: isStep0Valid = name.trim().length > 0 && expectedCloseDate.length > 0 && typeId.length > 0;
$: isStep1Valid = primarySalesRepId.length > 0 && companyId.length > 0; $: isStep1Valid = primarySalesRepId.length > 0 && companyId.length > 0;
$: isStep2Valid = true; // Contact & Site step is optional $: isStep2Valid = true; // Contact & Site step is optional
$: isValid = isStep0Valid && isStep1Valid && isStep2Valid; $: isStep3Valid = true; // Schedule Entry step is optional (can be skipped)
$: isValid = isStep0Valid && isStep1Valid && isStep2Valid && isStep3Valid;
$: selectedPrimaryRep = $cwMembers.find( $: selectedPrimaryRep = $cwMembers.find(
(m) => String(m.id) === primarySalesRepId, (m) => String(m.id) === primarySalesRepId,
@@ -112,6 +171,11 @@
(c) => String(c.cwId) === selectedContactId, (c) => String(c.cwId) === selectedContactId,
); );
$: activeContacts = contacts.filter((c) => !c.inactive); $: activeContacts = contacts.filter((c) => !c.inactive);
$: activeSites = allAddresses.filter((a) => !a.inactiveFlag);
$: selectedSite =
selectedSiteUid !== null
? (activeSites.find((a) => a.uid === selectedSiteUid) ?? null)
: (activeSites.find((a) => a.defaultFlag) ?? activeSites[0] ?? null);
// ── Default close date to 30 days from now ── // ── Default close date to 30 days from now ──
$: if (isOpen && !expectedCloseDate) { $: if (isOpen && !expectedCloseDate) {
@@ -211,6 +275,8 @@
contacts = []; contacts = [];
selectedContactId = ""; selectedContactId = "";
companyAddress = null; companyAddress = null;
allAddresses = [];
selectedSiteUid = null;
} }
function handleCompanyKeydown(e: KeyboardEvent) { function handleCompanyKeydown(e: KeyboardEvent) {
@@ -247,6 +313,8 @@
contacts = []; contacts = [];
selectedContactId = ""; selectedContactId = "";
companyAddress = null; companyAddress = null;
allAddresses = [];
selectedSiteUid = null;
} }
// ── Load company details (contacts + address) ── // ── Load company details (contacts + address) ──
@@ -259,6 +327,7 @@
cw_Data?: { cw_Data?: {
allContacts?: CompanyContact[]; allContacts?: CompanyContact[];
address?: CompanyAddress; address?: CompanyAddress;
allAddresses?: CompanySite[];
}; };
}; };
}>(`/api/companies/${companyOptimaId}/details`); }>(`/api/companies/${companyOptimaId}/details`);
@@ -268,6 +337,12 @@
const allContacts: CompanyContact[] = data.cw_Data?.allContacts ?? []; const allContacts: CompanyContact[] = data.cw_Data?.allContacts ?? [];
contacts = allContacts; contacts = allContacts;
companyAddress = data.cw_Data?.address ?? null; companyAddress = data.cw_Data?.address ?? null;
allAddresses = data.cw_Data?.allAddresses ?? [];
// Auto-select default/first active site
const activeAddr = allAddresses.filter((a) => !a.inactiveFlag);
const defaultAddr = activeAddr.find((a) => a.defaultFlag) ?? activeAddr[0];
selectedSiteUid = defaultAddr?.uid ?? null;
// Auto-select first active contact as default // Auto-select first active contact as default
const active = allContacts.filter((c: CompanyContact) => !c.inactive); const active = allContacts.filter((c: CompanyContact) => !c.inactive);
@@ -288,10 +363,11 @@
function nextStep() { function nextStep() {
if (currentStep === 0 && !isStep0Valid) return; if (currentStep === 0 && !isStep0Valid) return;
if (currentStep === 1 && !isStep1Valid) return; if (currentStep === 1 && !isStep1Valid) return;
if (currentStep < 3) { if (currentStep < 4) {
currentStep++; currentStep++;
if (currentStep === 1) loadMembers(); if (currentStep === 1) loadMembers();
if (currentStep === 2) loadCompanyDetails(); if (currentStep === 2) loadCompanyDetails();
if (currentStep === 3) initScheduleEntryTimes();
} }
} }
@@ -316,6 +392,11 @@
} }
if (step === 3 && isStep0Valid && isStep1Valid && isStep2Valid) { if (step === 3 && isStep0Valid && isStep1Valid && isStep2Valid) {
currentStep = 3; currentStep = 3;
initScheduleEntryTimes();
return;
}
if (step === 4 && isStep0Valid && isStep1Valid && isStep2Valid && isStep3Valid) {
currentStep = 4;
return; return;
} }
} }
@@ -358,6 +439,33 @@
console.warn("No opportunity ID in response:", JSON.stringify(result)); console.warn("No opportunity ID in response:", JSON.stringify(result));
} }
// ── Create schedule entry if the user filled one in ────────────────────
if (!skipScheduleEntry && newId) {
try {
await clientFetch(`/sales/opportunity/${newId}/workflow`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "createScheduleEntry",
payload: {
activityTypeValue: scheduleEntryType,
...(scheduleEntryEndDate ? { dueDate: scheduleEntryEndDate } : {}),
...(scheduleEntryStartDate && scheduleEntryStartHour
? { startTime: new Date(`${scheduleEntryStartDate}T${scheduleEntryStartHour}`).toISOString() }
: {}),
...(scheduleEntryEndDate && scheduleEntryEndHour
? { endTime: new Date(`${scheduleEntryEndDate}T${scheduleEntryEndHour}`).toISOString() }
: {}),
...(scheduleEntryNote.trim() ? { note: scheduleEntryNote.trim() } : {}),
},
}),
});
} catch {
// Non-fatal: the opportunity was created; log and continue.
console.warn("[CreateOpportunityModal] Schedule entry creation failed after opportunity was created.");
}
}
reset(); reset();
if (shouldNavigate) { if (shouldNavigate) {
@@ -393,11 +501,21 @@
contacts = []; contacts = [];
selectedContactId = ""; selectedContactId = "";
companyAddress = null; companyAddress = null;
allAddresses = [];
selectedSiteUid = null;
isLoadingCompanyDetails = false; isLoadingCompanyDetails = false;
isSubmitting = false; isSubmitting = false;
submitError = ""; submitError = "";
currentStep = 0; currentStep = 0;
navigateOnCreate = true; navigateOnCreate = true;
scheduleEntryType = "Follow-Up";
scheduleEntryStartDate = "";
scheduleEntryStartHour = "";
scheduleEntryEndDate = "";
scheduleEntryEndHour = "";
scheduleEntryNote = "";
skipScheduleEntry = false;
scheduleEntryTimeError = "";
} }
function handleClose() { function handleClose() {
@@ -482,11 +600,13 @@
class:completed={i < currentStep} class:completed={i < currentStep}
class:disabled={(i > 0 && !isStep0Valid) || class:disabled={(i > 0 && !isStep0Valid) ||
(i > 1 && !isStep1Valid) || (i > 1 && !isStep1Valid) ||
(i > 2 && !isStep2Valid)} (i > 2 && !isStep2Valid) ||
(i > 3 && !isStep3Valid)}
on:click={() => goToStep(i)} on:click={() => goToStep(i)}
disabled={(i > 0 && !isStep0Valid) || disabled={(i > 0 && !isStep0Valid) ||
(i > 1 && !isStep1Valid) || (i > 1 && !isStep1Valid) ||
(i > 2 && !isStep2Valid)} (i > 2 && !isStep2Valid) ||
(i > 3 && !isStep3Valid)}
> >
<span class="co-step-number" class:check={i < currentStep}> <span class="co-step-number" class:check={i < currentStep}>
{#if i < currentStep} {#if i < currentStep}
@@ -597,6 +717,21 @@
/> />
</div> </div>
<div class="co-form-group">
<label for="co-type">Opportunity Type <span class="co-req">*</span></label>
<select
id="co-type"
bind:value={typeId}
disabled={isSubmitting || isLoadingTypes}
class:has-value={!!typeId}
>
<option value="">— Select type —</option>
{#each resolvedTypes as t (t.id)}
<option value={String(t.id)}>{t.name}</option>
{/each}
</select>
</div>
<div class="co-form-group"> <div class="co-form-group">
<label for="co-source">Source</label> <label for="co-source">Source</label>
<input <input
@@ -618,21 +753,6 @@
disabled={isSubmitting} disabled={isSubmitting}
/> />
</div> </div>
<div class="co-form-group">
<label for="co-type">Opportunity Type</label>
<select
id="co-type"
bind:value={typeId}
disabled={isSubmitting || isLoadingTypes}
class:has-value={!!typeId}
>
<option value="">— Select type (optional) —</option>
{#each resolvedTypes as t (t.id)}
<option value={String(t.id)}>{t.name}</option>
{/each}
</select>
</div>
</div> </div>
</div> </div>
@@ -1116,39 +1236,63 @@
</svg> </svg>
Loading site information… Loading site information…
</div> </div>
{:else if companyAddress} {:else if activeSites.length > 0}
<div class="co-site-card"> {#if activeSites.length > 1}
<div class="co-site-card-icon"> <div class="co-form-grid">
<svg <div class="co-form-group co-full-width">
viewBox="0 0 24 24" <label for="co-site-select">Site</label>
fill="none" <select
stroke="currentColor" id="co-site-select"
stroke-width="1.5" bind:value={selectedSiteUid}
width="20" disabled={isSubmitting}
height="20" >
> {#each activeSites as site}
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" /> <option value={site.uid}>
<circle cx="12" cy="10" r="3" /> {site.name || site.addressLine1 || "Unnamed Site"}{site.defaultFlag ? " (Default)" : ""}
</svg> </option>
{/each}
</select>
</div>
</div> </div>
<div class="co-site-card-details"> {/if}
<p class="co-site-label">Primary Address</p> {#if selectedSite}
<p class="co-site-line">{companyAddress.line1 ?? ""}</p> <div class="co-site-card">
{#if companyAddress.line2} <div class="co-site-card-icon">
<p class="co-site-line">{companyAddress.line2}</p> <svg
{/if} viewBox="0 0 24 24"
<p class="co-site-line"> fill="none"
{companyAddress.city ?? ""}{companyAddress.city && stroke="currentColor"
companyAddress.state stroke-width="1.5"
? ", " width="20"
: ""}{companyAddress.state ?? ""} height="20"
{companyAddress.zip ?? ""} >
</p> <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
{#if companyAddress.country && companyAddress.country !== "United States"} <circle cx="12" cy="10" r="3" />
<p class="co-site-line">{companyAddress.country}</p> </svg>
{/if} </div>
<div class="co-site-card-details">
{#if selectedSite.name}
<p class="co-site-label">{selectedSite.name}</p>
{/if}
{#if selectedSite.addressLine1}
<p class="co-site-line">{selectedSite.addressLine1}</p>
{/if}
{#if selectedSite.addressLine2}
<p class="co-site-line">{selectedSite.addressLine2}</p>
{/if}
<p class="co-site-line">
{selectedSite.city ?? ""}{selectedSite.city && selectedSite.state ? ", " : ""}{selectedSite.state ?? ""}
{selectedSite.zip ?? ""}
</p>
{#if selectedSite.country && selectedSite.country !== "United States"}
<p class="co-site-line">{selectedSite.country}</p>
{/if}
{#if selectedSite.phone}
<p class="co-site-line">{selectedSite.phone}</p>
{/if}
</div>
</div> </div>
</div> {/if}
{:else} {:else}
<div class="co-empty-state"> <div class="co-empty-state">
<svg <svg
@@ -1168,8 +1312,136 @@
</div> </div>
</div> </div>
<!-- Step 3: Review --> <!-- Step 3: Schedule Entry -->
{:else if currentStep === 3} {:else if currentStep === 3}
<div class="co-step-content" style="animation: coFadeIn 0.2s ease">
<div class="co-section">
<h3 class="co-section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
Initial Schedule Entry
</h3>
<p class="co-step-hint">
Optionally create a schedule entry for this opportunity right away. You can skip this and add entries later.
</p>
<label class="co-skip-check">
<input type="checkbox" bind:checked={skipScheduleEntry} />
<span>Skip — I'll add a schedule entry later</span>
</label>
{#if !skipScheduleEntry}
<div class="co-form-grid co-schedule-grid">
<!-- Activity type -->
<div class="co-form-group co-full-width">
<label for="co-se-type">Activity Type <span class="co-req">*</span></label>
<div class="co-se-type-pills">
{#each (["Follow-Up", "Appointment", "Admin"] as const) as t}
<button
type="button"
class="co-se-pill"
class:co-se-pill-active={scheduleEntryType === t}
on:click={() => (scheduleEntryType = t)}
>
{#if t === "Follow-Up"}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
<path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.908.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/>
</svg>
{:else if t === "Appointment"}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
{:else}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
{/if}
{t}
</button>
{/each}
</div>
</div>
<!-- Start date -->
<div class="co-form-group">
<label for="co-se-start-date">Start Date</label>
<input
id="co-se-start-date"
type="date"
class:co-input-error={!!scheduleEntryTimeError}
bind:value={scheduleEntryStartDate}
disabled={isSubmitting}
on:change={() => { scheduleEntryTimeError = ""; }}
/>
</div>
<!-- Start time -->
<div class="co-form-group">
<label for="co-se-start-hour">Start Time</label>
<input
id="co-se-start-hour"
type="time"
class:co-input-error={!!scheduleEntryTimeError}
bind:value={scheduleEntryStartHour}
disabled={isSubmitting}
on:change={() => { scheduleEntryTimeError = ""; }}
/>
</div>
<!-- End date -->
<div class="co-form-group">
<label for="co-se-end-date">End Date</label>
<input
id="co-se-end-date"
type="date"
class:co-input-error={!!scheduleEntryTimeError}
bind:value={scheduleEntryEndDate}
disabled={isSubmitting}
on:change={() => { scheduleEntryTimeError = ""; }}
/>
</div>
<!-- End time -->
<div class="co-form-group">
<label for="co-se-end-hour">End Time</label>
<input
id="co-se-end-hour"
type="time"
class:co-input-error={!!scheduleEntryTimeError}
bind:value={scheduleEntryEndHour}
disabled={isSubmitting}
on:change={() => { scheduleEntryTimeError = ""; }}
/>
</div>
{#if scheduleEntryTimeError}
<div class="co-full-width co-field-error">{scheduleEntryTimeError}</div>
{/if}
<!-- Notes -->
<div class="co-form-group co-full-width">
<label for="co-se-notes">Notes <span class="co-optional-label">(optional)</span></label>
<textarea
id="co-se-notes"
class="co-notes-textarea"
bind:value={scheduleEntryNote}
placeholder="Add any notes for this schedule entry…"
rows="3"
disabled={isSubmitting}
></textarea>
</div>
</div>
{/if}
</div>
</div>
<!-- Step 4: Review -->
{:else if currentStep === 4}
<div class="co-step-content" style="animation: coFadeIn 0.2s ease"> <div class="co-step-content" style="animation: coFadeIn 0.2s ease">
<div class="co-review"> <div class="co-review">
<div class="co-review-header"> <div class="co-review-header">
@@ -1221,6 +1493,12 @@
})} })}
</dd> </dd>
</div> </div>
{#if selectedTypeName}
<div class="co-review-item">
<dt>Type</dt>
<dd>{selectedTypeName}</dd>
</div>
{/if}
{#if source} {#if source}
<div class="co-review-item"> <div class="co-review-item">
<dt>Source</dt> <dt>Source</dt>
@@ -1293,14 +1571,13 @@
: "—"} : "—"}
</dd> </dd>
</div> </div>
{#if companyAddress} {#if selectedSite}
<div class="co-review-item"> <div class="co-review-item">
<dt>Site</dt> <dt>Site</dt>
<dd> <dd>
{companyAddress.city ?? ""}{companyAddress.city && {selectedSite.name
companyAddress.state ? selectedSite.name
? ", " : `${selectedSite.city ?? ""}${selectedSite.city && selectedSite.state ? ", " : ""}${selectedSite.state ?? ""}`}
: ""}{companyAddress.state ?? ""}
</dd> </dd>
</div> </div>
{/if} {/if}
@@ -1332,6 +1609,39 @@
</div> </div>
{/if} {/if}
<!-- Schedule entry review card -->
{#if !skipScheduleEntry}
<div class="co-review-notes co-review-schedule">
<h4>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
Schedule Entry
</h4>
<dl class="co-review-schedule-dl">
<div class="co-review-item">
<dt>Type</dt>
<dd>{scheduleEntryType}</dd>
</div>
{#if scheduleEntryStartDate && scheduleEntryStartHour}
<div class="co-review-item">
<dt>Start</dt>
<dd>{new Date(`${scheduleEntryStartDate}T${scheduleEntryStartHour}`).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</dd>
</div>
{/if}
{#if scheduleEntryEndDate && scheduleEntryEndHour}
<div class="co-review-item">
<dt>End</dt>
<dd>{new Date(`${scheduleEntryEndDate}T${scheduleEntryEndHour}`).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</dd>
</div>
{/if}
</dl>
</div>
{:else}
<p class="co-review-skipped">No schedule entry — will be added later.</p>
{/if}
<label class="co-navigate-check"> <label class="co-navigate-check">
<input type="checkbox" bind:checked={navigateOnCreate} /> <input type="checkbox" bind:checked={navigateOnCreate} />
<span>Open opportunity after creating</span> <span>Open opportunity after creating</span>
@@ -1375,20 +1685,23 @@
Cancel Cancel
</button> </button>
{#if currentStep < 3} {#if currentStep < 4}
<button <button
class="co-btn-next" class="co-btn-next"
on:click={nextStep} on:click={nextStep}
disabled={(currentStep === 0 && !isStep0Valid) || disabled={(currentStep === 0 && !isStep0Valid) ||
(currentStep === 1 && !isStep1Valid) || (currentStep === 1 && !isStep1Valid) ||
(currentStep === 2 && !isStep2Valid)} (currentStep === 2 && !isStep2Valid) ||
(currentStep === 3 && !isStep3Valid)}
type="button" type="button"
> >
{currentStep === 0 {currentStep === 0
? "Next: Assignment" ? "Next: Assignment"
: currentStep === 1 : currentStep === 1
? "Next: Contact & Site" ? "Next: Contact & Site"
: "Review"} : currentStep === 2
? "Next: Schedule Entry"
: "Review"}
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
@@ -1473,7 +1786,7 @@
.co-modal { .co-modal {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 640px; width: 740px;
max-width: 94vw; max-width: 94vw;
max-height: 88vh; max-height: 88vh;
background: var(--bg-surface, #ffffff); background: var(--bg-surface, #ffffff);
@@ -1560,21 +1873,23 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0; gap: 0;
padding: 16px 24px; padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle, #e5e5e5); border-bottom: 1px solid var(--border-subtle, #e5e5e5);
background: var(--bg-inset, #fafafa); background: var(--bg-inset, #fafafa);
overflow: hidden;
} }
.co-step { .co-step {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 5px;
padding: 6px 14px; padding: 5px 8px;
border: none; border: none;
background: none; background: none;
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px;
transition: all 0.15s; transition: all 0.15s;
white-space: nowrap;
} }
.co-step:hover:not(.disabled) { .co-step:hover:not(.disabled) {
@@ -1615,7 +1930,7 @@
} }
.co-step-label { .co-step-label {
font-size: 12.5px; font-size: 11.5px;
font-weight: 550; font-weight: 550;
color: var(--text-secondary, #666); color: var(--text-secondary, #666);
transition: color 0.15s; transition: color 0.15s;
@@ -1626,11 +1941,12 @@
} }
.co-step-connector { .co-step-connector {
width: 32px; width: 20px;
height: 2px; height: 2px;
background: var(--border-subtle, #ddd); background: var(--border-subtle, #ddd);
border-radius: 1px; border-radius: 1px;
transition: background 0.2s; transition: background 0.2s;
flex-shrink: 0;
} }
.co-step-connector.filled { .co-step-connector.filled {
@@ -1704,6 +2020,7 @@
.co-form-group input[type="text"], .co-form-group input[type="text"],
.co-form-group input[type="date"], .co-form-group input[type="date"],
.co-form-group input[type="time"],
.co-form-group select { .co-form-group select {
width: 100%; width: 100%;
padding: 8px 12px; padding: 8px 12px;
@@ -2197,6 +2514,136 @@
color: var(--text-secondary, #666); color: var(--text-secondary, #666);
} }
.co-navigate-check {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 8px;
background: var(--bg-inset, #fafafa);
border: 1px solid var(--border-subtle, #e5e5e5);
cursor: pointer;
transition: background 0.1s;
}
.co-navigate-check:hover {
background: var(--card-hover-bg, #f0f0f0);
}
.co-navigate-check input {
accent-color: #3b82f6;
}
.co-navigate-check span {
font-size: 12.5px;
font-weight: 500;
color: var(--text-secondary, #666);
}
/* ── Schedule Entry step ── */
.co-step-hint {
font-size: 12.5px;
color: var(--text-muted, #888);
margin-bottom: 12px;
line-height: 1.5;
}
.co-skip-check {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 8px;
background: var(--bg-inset, #1a1a1a);
border: 1px solid var(--border-subtle, #333);
cursor: pointer;
margin-bottom: 16px;
transition: background 0.1s;
}
.co-skip-check:hover {
background: var(--card-hover-bg, #222);
}
.co-skip-check input {
accent-color: #3b82f6;
}
.co-skip-check span {
font-size: 12.5px;
font-weight: 500;
color: var(--text-secondary, #aaa);
}
.co-schedule-grid {
margin-top: 4px;
}
.co-se-type-pills {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.co-se-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 14px;
border-radius: 20px;
border: 1px solid var(--border-color, #444);
background: transparent;
color: var(--text-muted, #888);
font-size: 13px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.co-se-pill:hover {
border-color: var(--accent-color, #4f8ef7);
color: var(--accent-color, #4f8ef7);
}
.co-se-pill.co-se-pill-active {
border-color: var(--accent-color, #4f8ef7);
background: var(--accent-bg, rgba(79, 142, 247, 0.1));
color: var(--accent-color, #4f8ef7);
font-weight: 600;
}
.co-optional-label {
font-weight: 400;
color: var(--text-muted, #888);
font-size: 11px;
}
.co-input-error {
border-color: #ef4444 !important;
}
.co-field-error {
font-size: 11.5px;
color: #ef4444;
margin-top: -4px;
margin-bottom: 4px;
}
/* ── Schedule entry review card ── */
.co-review-schedule {
margin-top: 12px;
}
.co-review-schedule-dl {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
margin-top: 8px;
}
.co-review-skipped {
font-size: 12.5px;
color: var(--text-muted, #888);
font-style: italic;
margin: 8px 0;
}
/* ── Error banner ── */ /* ── Error banner ── */
.co-error-banner { .co-error-banner {
display: flex; display: flex;
@@ -2459,6 +2906,7 @@
.co-form-group input[type="text"], .co-form-group input[type="text"],
.co-form-group input[type="date"], .co-form-group input[type="date"],
.co-form-group input[type="time"],
.co-form-group select { .co-form-group select {
font-size: 16px; font-size: 16px;
padding: 10px 12px; padding: 10px 12px;
+274
View File
@@ -0,0 +1,274 @@
<script lang="ts">
import { clientFetch } from "$lib/client-fetch";
import { onDestroy } from "svelte";
export let identifier: string;
export let onHand: number | undefined;
// Warehouse IDs for the fixed locations
// Murray = Andrus 301 (1) + Andrus-205C (26) + Andrus 205D (27)
const MURRAY_IDS = new Set([1, 26, 27]);
const UNION_CITY_ID = 6;
const LONDON_ID = 30;
type InventoryRow = {
id: number;
qtyOnHand: number;
warehouseId: number | null;
warehouseBinId: number;
warehouse: { id: number; name: string } | null;
};
type Breakdown = {
murray: number;
unionCity: number;
london: number;
vehicles: number;
};
let visible = false;
let loading = false;
let breakdown: Breakdown | null = null;
let fetchedFor: string | null = null;
let anchorEl: HTMLElement;
let popoverX = 0;
let popoverY = 0;
// Portal action — moves the node to document.body so it is never
// clipped by a parent overflow or trapped inside a CSS transform context.
function portal(node: HTMLElement) {
document.body.appendChild(node);
return {
destroy() {
node.parentNode?.removeChild(node);
},
};
}
function onhandClass(qty: number) {
if (qty === 0) return "onhand-zero";
if (qty <= 3) return "onhand-low";
return "onhand-ok";
}
function computeBreakdown(rows: InventoryRow[]): Breakdown {
const result: Breakdown = { murray: 0, unionCity: 0, london: 0, vehicles: 0 };
for (const row of rows) {
const wId = row.warehouseId;
const wName = row.warehouse?.name ?? "";
if (wId != null && MURRAY_IDS.has(wId)) {
result.murray += row.qtyOnHand;
} else if (wId === UNION_CITY_ID) {
result.unionCity += row.qtyOnHand;
} else if (wId === LONDON_ID) {
result.london += row.qtyOnHand;
} else if (/^\d+$/.test(wName)) {
result.vehicles += row.qtyOnHand;
}
}
return result;
}
async function load() {
if (fetchedFor === identifier) return;
loading = true;
breakdown = null;
try {
const result = await clientFetch<{ data: InventoryRow[] }>(
`/procurement/catalog/inventory?id=${encodeURIComponent(identifier)}`,
);
breakdown = computeBreakdown(result?.data ?? []);
fetchedFor = identifier;
} catch {
breakdown = { murray: 0, unionCity: 0, london: 0, vehicles: 0 };
fetchedFor = identifier;
} finally {
loading = false;
}
}
function updatePosition() {
if (!anchorEl) return;
const rect = anchorEl.getBoundingClientRect();
popoverX = rect.left + rect.width / 2;
popoverY = rect.top - 6;
}
function handleMouseEnter() {
updatePosition();
visible = true;
load();
}
function handleMouseLeave() {
visible = false;
}
onDestroy(() => {
visible = false;
});
</script>
<div
class="inv-popover-anchor"
role="button"
tabindex="0"
bind:this={anchorEl}
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
on:focusin={handleMouseEnter}
on:focusout={handleMouseLeave}
>
<span
class="onhand-badge"
class:onhand-zero={onHand === 0}
class:onhand-low={onHand != null && onHand > 0 && onHand <= 3}
class:onhand-ok={onHand != null && onHand > 3}
>
{onHand ?? "—"}
</span>
</div>
{#if visible}
<div
use:portal
class="inv-popover"
role="tooltip"
style="left:{popoverX}px; top:{popoverY}px;"
>
<div class="inv-popover-title">Inventory Breakdown</div>
{#if loading}
<div class="inv-popover-loading">
<span class="inv-spinner" />
Loading…
</div>
{:else if breakdown}
<div class="inv-popover-rows">
<div class="inv-popover-row">
<span class="inv-location">Murray, KY</span>
<span class="onhand-badge {onhandClass(breakdown.murray)}">{breakdown.murray}</span>
</div>
<div class="inv-popover-row">
<span class="inv-location">Union City, TN</span>
<span class="onhand-badge {onhandClass(breakdown.unionCity)}">{breakdown.unionCity}</span>
</div>
<div class="inv-popover-row">
<span class="inv-location">London, KY</span>
<span class="onhand-badge {onhandClass(breakdown.london)}">{breakdown.london}</span>
</div>
<div class="inv-popover-row">
<span class="inv-location">Vehicles</span>
<span class="onhand-badge {onhandClass(breakdown.vehicles)}">{breakdown.vehicles}</span>
</div>
</div>
{/if}
</div>
{/if}
<style>
.inv-popover-anchor {
position: relative;
display: inline-block;
cursor: default;
}
/* Badge styles — self-contained so component works outside catalog.css context */
.onhand-badge {
display: inline-block;
min-width: 28px;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
text-align: center;
}
.onhand-zero {
background: var(--status-inactive-bg, #fee2e2);
color: var(--status-inactive-color, #dc2626);
}
.onhand-low {
background: var(--status-pending-bg, #fef3c7);
color: var(--status-pending-color, #d97706);
}
.onhand-ok {
background: var(--status-active-bg, #dcfce7);
color: var(--status-active-color, #16a34a);
}
/*
* Portalled to document.body — fixed to viewport, above everything.
* Note: Svelte scoped styles won't apply to portalled nodes, so the
* popover styles below use :global() to reach across the portal boundary.
*/
:global(.inv-popover) {
position: fixed;
transform: translateX(-50%) translateY(-100%);
z-index: 9999;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 8px;
box-shadow: var(--card-hover-shadow, 0 8px 24px rgba(0, 0, 0, 0.15));
padding: 10px 14px;
min-width: 210px;
white-space: nowrap;
pointer-events: none;
font-family: inherit;
}
:global(.inv-popover-title) {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 8px;
}
:global(.inv-popover-loading) {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
padding: 4px 0;
}
:global(.inv-spinner) {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid var(--border-subtle);
border-top-color: var(--text-secondary);
border-radius: 50%;
animation: inv-spin 0.6s linear infinite;
flex-shrink: 0;
}
@keyframes inv-spin {
to { transform: rotate(360deg); }
}
:global(.inv-popover-rows) {
display: flex;
flex-direction: column;
gap: 6px;
}
:global(.inv-popover-row) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
:global(.inv-location) {
font-size: 12px;
color: var(--text-primary);
font-weight: 500;
}
</style>
+3 -52
View File
@@ -3,64 +3,15 @@
export let size: number = 160; export let size: number = 160;
</script> </script>
<div class="monkey" style="width: {size}px"> <div class="empty-state">
<svg viewBox="0 0 120 120" width="100%" height="100%" aria-hidden="true"> <p class="msg">{message}</p>
<!-- head -->
<circle cx="60" cy="60" r="44" fill="#8B5E3C" />
<!-- face -->
<ellipse cx="60" cy="70" rx="30" ry="24" fill="#E8C9A1" />
<!-- ears -->
<circle cx="26" cy="56" r="12" fill="#8B5E3C" />
<circle cx="94" cy="56" r="12" fill="#8B5E3C" />
<circle cx="26" cy="56" r="6" fill="#E8C9A1" />
<circle cx="94" cy="56" r="6" fill="#E8C9A1" />
<!-- eyes -->
<circle cx="48" cy="64" r="6" fill="#2b2b2b" />
<circle cx="72" cy="64" r="6" fill="#2b2b2b" />
<!-- smile -->
<path
d="M48 78 Q60 88 72 78"
stroke="#2b2b2b"
stroke-width="3"
fill="none"
stroke-linecap="round"
/>
<!-- eyebrow accents -->
<path
d="M42 58 Q48 54 54 58"
stroke="#6b4a33"
stroke-width="2"
fill="none"
stroke-linecap="round"
opacity="0.6"
/>
<path
d="M66 58 Q72 54 78 58"
stroke="#6b4a33"
stroke-width="2"
fill="none"
stroke-linecap="round"
opacity="0.6"
/>
<!-- tiny tuft -->
<path
d="M60 28 Q58 34 62 36"
stroke="#6b4a33"
stroke-width="3"
fill="none"
stroke-linecap="round"
/>
</svg>
<div class="msg">{message}</div>
</div> </div>
<style> <style>
.monkey { .empty-state {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.75rem;
margin: 1.5rem auto; margin: 1.5rem auto;
} }
.msg { .msg {
@@ -1,33 +0,0 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/svelte";
import NoResultsMonkey from "./NoResultsMonkey.svelte";
describe("NoResultsMonkey", () => {
it("renders with default message", () => {
render(NoResultsMonkey);
expect(screen.getByText("No results found")).toBeInTheDocument();
});
it("renders with custom message", () => {
render(NoResultsMonkey, { props: { message: "Nothing here" } });
expect(screen.getByText("Nothing here")).toBeInTheDocument();
});
it("renders SVG illustration", () => {
const { container } = render(NoResultsMonkey);
const svg = container.querySelector("svg");
expect(svg).toBeInTheDocument();
});
it("applies custom size", () => {
const { container } = render(NoResultsMonkey, {
props: { size: 200 },
});
const wrapper = container.querySelector(".monkey");
expect(wrapper).toHaveStyle("width: 200px");
});
});
@@ -118,9 +118,9 @@
const ACTION_LABELS: Partial<Record<WorkflowAction, string>> = { const ACTION_LABELS: Partial<Record<WorkflowAction, string>> = {
requestReview: "Request Review", requestReview: "Request Review",
reviewDecision: "Complete Review", reviewDecision: "Complete Review",
sendQuote: "Quote Sent", sendQuote: "Mark Quote Sent",
ReadyToSend: "Ready to Send", ReadyToSend: "Ready to Send",
resendQuote: "Quote Resent", resendQuote: "Mark Quote Resent",
confirmQuote: "Quote Confirmed", confirmQuote: "Quote Confirmed",
beginRevision: "Begin Revising", beginRevision: "Begin Revising",
resurrect: "Resurrect", resurrect: "Resurrect",
@@ -8,12 +8,16 @@ export const company = {
includeAddress?: boolean; includeAddress?: boolean;
includePrimaryContact?: boolean; includePrimaryContact?: boolean;
includeAllContacts?: boolean; includeAllContacts?: boolean;
includeAllAddresses?: boolean;
}, },
) { ) {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (options?.includeAddress) params.includeAddress = "true"; if (options?.includeAddress) params.includeAddress = "true";
if (options?.includePrimaryContact) params.includePrimaryContact = "true"; if (options?.includePrimaryContact) params.includePrimaryContact = "true";
if (options?.includeAllContacts) params.includeAllContacts = "true"; if (options?.includeAllContacts) params.includeAllContacts = "true";
if (options?.includeAllAddresses) params.includeAllAddresses = "true";
const company = await api.get(`/v1/company/companies/${id}`, { const company = await api.get(`/v1/company/companies/${id}`, {
params, params,
@@ -21,6 +25,8 @@ export const company = {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}, },
}); });
console.log(company.data);
return company.data; return company.data;
}, },
async fetchMany( async fetchMany(
@@ -146,6 +146,19 @@ export const procurement = {
return response.data.data.count; return response.data.data.count;
}, },
async fetchInventory(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/procurement/items/${identifier}/inventory`,
{
params: { includeWarehouse: true },
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
return response.data;
},
async refreshInventory(accessToken: string, identifier: string) { async refreshInventory(accessToken: string, identifier: string) {
const response = await api.post( const response = await api.post(
`/v1/procurement/items/${identifier}/refresh-inventory`, `/v1/procurement/items/${identifier}/refresh-inventory`,
+90 -4
View File
@@ -86,9 +86,9 @@ export interface SalesOpportunity {
closedFlag?: boolean; closedFlag?: boolean;
probability?: { id?: number; percent?: number } | null; probability?: { id?: number; percent?: number } | null;
closedBy?: closedBy?:
| string | string
| { id?: number | string; identifier?: string; name?: string } | { id?: number | string; identifier?: string; name?: string }
| null; | null;
companyId?: string; companyId?: string;
productSequence?: number[] | null; productSequence?: number[] | null;
cwLastUpdated?: string | null; cwLastUpdated?: string | null;
@@ -111,7 +111,9 @@ export type WorkflowAction =
| "beginRevision" | "beginRevision"
| "resendQuote" | "resendQuote"
| "cancel" | "cancel"
| "reopen"; | "reopen"
| "sendBackForRevision"
| "createScheduleEntry";
export type ReviewDecision = "approve" | "reject" | "send" | "cancel"; export type ReviewDecision = "approve" | "reject" | "send" | "cancel";
@@ -129,6 +131,11 @@ export interface WorkflowActionPayload {
// finalize // finalize
outcome?: "won" | "lost"; outcome?: "won" | "lost";
finalize?: boolean; finalize?: boolean;
// createScheduleEntry
activityTypeValue?: "Follow-Up" | "Appointment" | "Admin";
dueDate?: string;
startTime?: string;
endTime?: string;
} }
export interface WorkflowAvailableAction { export interface WorkflowAvailableAction {
@@ -199,12 +206,28 @@ export interface OpportunityActivity {
closedAt?: string; closedAt?: string;
} }
export interface WorkflowTimeEntry {
id: string;
cwId: number;
memberId?: string | null;
member?: { name: string; email: string | null } | null;
dateStart?: string | null;
timeStart?: string | null;
timeEnd?: string | null;
notes?: string | null;
actualHours?: number | null;
billableHours?: number | null;
billableFlag?: boolean | null;
}
export interface WorkflowHistoryEntry { export interface WorkflowHistoryEntry {
activity: OpportunityActivity; activity: OpportunityActivity;
optimaType: string; optimaType: string;
quoteId?: string | null; quoteId?: string | null;
parentActivityId?: number | null;
closed?: boolean; closed?: boolean;
closedAt?: string | null; closedAt?: string | null;
timeEntries?: WorkflowTimeEntry[];
} }
export interface WorkflowHistoryResponse { export interface WorkflowHistoryResponse {
@@ -273,6 +296,24 @@ export const REOPENABLE_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
"Canceled", "Canceled",
]); ]);
/** Statuses where opportunity data is fully editable (frontend + backend). */
export const EDITABLE_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
"New",
"Active",
]);
/**
* Statuses where the "Add Time" button is hidden.
* PendingWon, PendingLost, Won, Lost, Canceled.
*/
export const ADD_TIME_EXCLUDED_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
"PendingWon",
"PendingLost",
"Won",
"Lost",
"Canceled",
]);
/** Statuses where the quote has been confirmed (finalize is allowed) */ /** Statuses where the quote has been confirmed (finalize is allowed) */
export const QUOTE_CONFIRMED_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set( export const QUOTE_CONFIRMED_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set(
["ConfirmedQuote", "ReadyToSend", "Active", "PendingWon", "PendingLost"] ["ConfirmedQuote", "ReadyToSend", "Active", "PendingWon", "PendingLost"]
@@ -613,6 +654,9 @@ export const sales = {
}, },
async fetchProducts(accessToken: string, identifier: string) { async fetchProducts(accessToken: string, identifier: string) {
console.log("fetch prod exec check")
const response = await api.get( const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent( `/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier identifier
@@ -623,6 +667,9 @@ export const sales = {
}, },
} }
); );
console.log("[fetchProducts] API response:", JSON.stringify(response.data, null, 2));
return response.data; return response.data;
}, },
@@ -907,6 +954,7 @@ export const sales = {
lineItemPricing?: boolean; lineItemPricing?: boolean;
includeQuoteNarrative?: boolean; includeQuoteNarrative?: boolean;
includeItemNarratives?: boolean; includeItemNarratives?: boolean;
separateRecurringServices?: boolean;
} }
) { ) {
const response = await api.post( const response = await api.post(
@@ -1162,4 +1210,42 @@ export const sales = {
successful?: boolean; successful?: boolean;
}; };
}, },
async fetchOpenActivities(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/activities`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
return response.data as {
successful?: boolean;
data: {
activities: Array<{
cwActivityId: number;
name: string;
optimaType: string | null;
parentActivityId: number | null;
status: { id?: number; name?: string } | null;
dateStart: string | null;
dateEnd: string | null;
}>;
};
};
},
async addTimeToActivity(
accessToken: string,
identifier: string,
payload: { activityId: number; timeStarted: string; timeEnded: string; notes?: string }
) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/time`,
payload,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
return response.data as {
successful?: boolean;
message?: string;
data: { cwTimeEntryId: number | null; activityId: number; activityWasClosed: boolean };
};
},
}; };
+1 -1
View File
@@ -60,7 +60,7 @@
<div class="layout-container"> <div class="layout-container">
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-content">
<h1>Project Optima</h1> <img src={$theme === 'dark' ? '/tts-logo-dark.png' : '/tts-logo.png'} alt="Total Tech Solutions" class="header-logo" />
<button <button
class="theme-toggle" class="theme-toggle"
on:click={() => theme.toggle()} on:click={() => theme.toggle()}
@@ -9,10 +9,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
return json({ data: null }, { status: 401 }); return json({ data: null }, { status: 401 });
} }
console.log("Here")
try { try {
const result = await optima.company.fetch(accessToken, params.id, { const result = await optima.company.fetch(accessToken, params.id, {
includeAllContacts: true, includeAllContacts: true,
includeAddress: true, includeAddress: true,
includeAllAddresses: true,
}); });
return json({ data: result?.data ?? null }); return json({ data: result?.data ?? null });
} catch (err) { } catch (err) {
@@ -46,6 +46,7 @@ describe("GET /api/companies/[id]/details", () => {
expect(mockOptima.company.fetch).toHaveBeenCalledWith("tok", "123", { expect(mockOptima.company.fetch).toHaveBeenCalledWith("tok", "123", {
includeAllContacts: true, includeAllContacts: true,
includeAddress: true, includeAddress: true,
includeAllAddresses: true,
}); });
expect(mockJson).toHaveBeenCalledWith({ expect(mockJson).toHaveBeenCalledWith({
data: { id: "123", name: "Acme" }, data: { id: "123", name: "Acme" },
@@ -51,6 +51,7 @@ export const load: PageServerLoad = async ({ locals, params }) => {
includeAddress: permissions["company.fetch.address"] === true, includeAddress: permissions["company.fetch.address"] === true,
includePrimaryContact: true, includePrimaryContact: true,
includeAllContacts: permissions["company.fetch.contacts"] === true, includeAllContacts: permissions["company.fetch.contacts"] === true,
includeAllAddresses: permissions["company.fetch.address"] === true,
}), }),
); );
@@ -15,6 +15,27 @@
export let permissions: PermissionMap; export let permissions: PermissionMap;
export let isMobile: boolean; export let isMobile: boolean;
export let mobileActiveTab: string | null; export let mobileActiveTab: string | null;
// Selected site for the sites dropdown
let selectedSiteUid: string | null = null;
$: allAddresses = company?.cw_Data?.allAddresses ?? [];
$: activeSites = allAddresses.filter((a) => !a.inactiveFlag);
$: selectedSite =
selectedSiteUid !== null
? activeSites.find((a) => a.uid === selectedSiteUid) ?? null
: activeSites.find((a) => a.defaultFlag) ?? activeSites[0] ?? null;
function formatSiteAddress(site: (typeof activeSites)[number]): string[] {
const lines: string[] = [];
if (site.addressLine1) lines.push(site.addressLine1);
if (site.addressLine2) lines.push(site.addressLine2);
const cityStateZip = [site.city, site.state, site.zip]
.filter(Boolean)
.join(", ");
if (cityStateZip) lines.push(cityStateZip);
if (site.country) lines.push(site.country);
return lines;
}
</script> </script>
<div <div
@@ -170,7 +191,54 @@
</div> </div>
{/if} {/if}
{#if permissions["company.fetch.address"] && formatAddress(company).length > 0} {#if permissions["company.fetch.address"] && activeSites.length > 0}
<div class="info-row">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
class="info-icon"
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
<div class="info-content">
{#if activeSites.length > 1}
<select
class="site-select"
bind:value={selectedSiteUid}
on:change={(e) => (selectedSiteUid = e.currentTarget.value)}
>
{#each activeSites as site}
<option value={site.uid}>
{site.name}{site.defaultFlag ? " (Default)" : ""}
</option>
{/each}
</select>
{:else}
<span class="info-label"
>{activeSites[0]?.name ?? "Address"}</span
>
{/if}
{#if selectedSite}
{@const lines = formatSiteAddress(selectedSite)}
{#if lines.length > 0}
<span class="info-value address-multiline">
{#each lines as line}
{line}<br />
{/each}
</span>
{/if}
{#if selectedSite.phone}
<span class="info-value site-phone"
>{formatPhone(selectedSite.phone)}</span
>
{/if}
{/if}
</div>
</div>
{:else if permissions["company.fetch.address"] && formatAddress(company).length > 0}
<div class="info-row"> <div class="info-row">
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -85,6 +85,7 @@ describe("companies/[id] +page.server.ts load", () => {
expect(mockOptima.company.fetch).toHaveBeenCalledWith("tok", "c1", { expect(mockOptima.company.fetch).toHaveBeenCalledWith("tok", "c1", {
includeAddress: true, includeAddress: true,
includeAllAddresses: true,
includePrimaryContact: true, includePrimaryContact: true,
includeAllContacts: true, includeAllContacts: true,
}); });
+15
View File
@@ -27,6 +27,21 @@ export interface CompanyData {
zip?: string; zip?: string;
country?: string; country?: string;
}; };
allAddresses?: Array<{
id: number;
uid: string;
name: string;
description?: string | null;
defaultFlag: boolean;
inactiveFlag: boolean;
addressLine1?: string | null;
addressLine2?: string | null;
city?: string | null;
state?: string | null;
zip?: string | null;
country?: string | null;
phone?: string | null;
}>;
primaryContact?: { primaryContact?: {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
+5 -43
View File
@@ -6,6 +6,7 @@
import NoResultsMonkey from "../../../components/NoResultsMonkey.svelte"; import NoResultsMonkey from "../../../components/NoResultsMonkey.svelte";
import AccessDenied from "../../../components/AccessDenied.svelte"; import AccessDenied from "../../../components/AccessDenied.svelte";
import Pagination from "../../../components/Pagination.svelte"; import Pagination from "../../../components/Pagination.svelte";
import InventoryPopover from "../../../components/InventoryPopover.svelte";
import { formatDate } from "$lib/utils"; import { formatDate } from "$lib/utils";
import "../../../styles/procurement/catalog.css"; import "../../../styles/procurement/catalog.css";
import { clientFetch } from "$lib/client-fetch"; import { clientFetch } from "$lib/client-fetch";
@@ -598,16 +599,7 @@
<td class="col-price">{formatCurrency(item.price)}</td> <td class="col-price">{formatCurrency(item.price)}</td>
<td class="col-cost">{formatCurrency(item.cost)}</td> <td class="col-cost">{formatCurrency(item.cost)}</td>
<td class="col-onhand"> <td class="col-onhand">
<span <InventoryPopover identifier={item.id} onHand={item.onHand} />
class="onhand-badge"
class:onhand-zero={item.onHand === 0}
class:onhand-low={item.onHand != null &&
item.onHand > 0 &&
item.onHand <= 3}
class:onhand-ok={item.onHand != null && item.onHand > 3}
>
{item.onHand ?? "—"}
</span>
</td> </td>
<td class="col-status"> <td class="col-status">
<span <span
@@ -721,17 +713,7 @@
<div class="detail-field"> <div class="detail-field">
<span class="detail-label">On Hand</span> <span class="detail-label">On Hand</span>
<span class="detail-value"> <span class="detail-value">
<span <InventoryPopover identifier={selectedItem.id} onHand={selectedItem.onHand} />
class="onhand-badge"
class:onhand-zero={selectedItem.onHand === 0}
class:onhand-low={selectedItem.onHand != null &&
selectedItem.onHand > 0 &&
selectedItem.onHand <= 3}
class:onhand-ok={selectedItem.onHand != null &&
selectedItem.onHand > 3}
>
{selectedItem.onHand ?? "—"}
</span>
</span> </span>
</div> </div>
</div> </div>
@@ -966,17 +948,7 @@
<div class="linked-detail-field"> <div class="linked-detail-field">
<span class="detail-label">On Hand</span> <span class="detail-label">On Hand</span>
<span class="detail-value"> <span class="detail-value">
<span <InventoryPopover identifier={li.id} onHand={li.onHand} />
class="onhand-badge"
class:onhand-zero={li.onHand === 0}
class:onhand-low={li.onHand != null &&
li.onHand > 0 &&
li.onHand <= 3}
class:onhand-ok={li.onHand != null &&
li.onHand > 3}
>
{li.onHand ?? "—"}
</span>
</span> </span>
</div> </div>
<div class="linked-detail-field"> <div class="linked-detail-field">
@@ -1332,17 +1304,7 @@
<div class="link-preview-field"> <div class="link-preview-field">
<span class="detail-label">On Hand</span> <span class="detail-label">On Hand</span>
<span class="detail-value"> <span class="detail-value">
<span <InventoryPopover identifier={linkPreviewItem.id} onHand={linkPreviewItem.onHand} />
class="onhand-badge"
class:onhand-zero={linkPreviewItem.onHand === 0}
class:onhand-low={linkPreviewItem.onHand != null &&
linkPreviewItem.onHand > 0 &&
linkPreviewItem.onHand <= 3}
class:onhand-ok={linkPreviewItem.onHand != null &&
linkPreviewItem.onHand > 3}
>
{linkPreviewItem.onHand ?? "—"}
</span>
</span> </span>
</div> </div>
{#if linkPreviewItem.manufacturer} {#if linkPreviewItem.manufacturer}
@@ -0,0 +1,24 @@
import { optima } from "$lib";
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
/** GET /procurement/catalog/inventory?id=<identifier> */
export const GET: RequestHandler = async ({ url, locals }) => {
const accessToken = locals.session?.accessToken;
if (!accessToken) throw error(401, "Unauthorized");
const identifier = url.searchParams.get("id");
if (!identifier) throw error(400, "Missing id parameter");
try {
const result = await optima.procurement.fetchInventory(accessToken, identifier);
return json(result);
} catch (err: unknown) {
console.error("Failed to fetch product inventory:", err);
const status =
err && typeof err === "object" && "status" in err
? (err as { status: number }).status
: 500;
throw error(status, "Failed to fetch product inventory");
}
};

Some files were not shown because too many files have changed in this diff Show More