feat: add time entry manager, controller, and API routes

This commit is contained in:
2026-04-21 00:52:35 +00:00
parent 38654601c9
commit a55850e2c1
39 changed files with 4700 additions and 440 deletions
+376 -110
View File
@@ -666,6 +666,7 @@ model Member {
approvedOpportunities Opportunity[] @relation("OpportunityApprovedBy")
rejectedOpportunities Opportunity[] @relation("OpportunityRejectedBy")
opportunityMembers OpportunityMember[] @relation("OpportunityMemberToMember")
memberType MemberType? @relation(fields: [memberTypeRecId], references: [memberTypeRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("Member")
@@schema("dbo")
@@ -1740,10 +1741,13 @@ model ActivityType {
description String @map("Description") @db.NVarChar(50)
hoursMin Decimal? @map("Hours_Min") @db.Decimal(18, 2)
hoursMax Decimal? @map("Hours_Max") @db.Decimal(18, 2)
defaultFlag Boolean @map("Default_Flag")
multiplierFlag Boolean @map("Multiplier_Flag")
rate Decimal? @map("Rate") @db.Decimal(18, 2)
rateType String? @map("Rate_Type") @db.Char(1)
defaultFlag Boolean @map("Default_Flag")
multiplierFlag Boolean @map("Multiplier_Flag")
rate Decimal? @map("Rate") @db.Decimal(18, 2)
rateType String? @map("Rate_Type") @db.Char(1)
inactiveFlag Boolean @map("Inactive_Flag")
invoiceFlag Boolean @map("Invoice_Flag")
lastUpdate DateTime @map("Last_Update") @db.DateTime2
@@ -2069,45 +2073,63 @@ model Country {
// =====================
model SoActivity {
soActivityRecId Int @id @map("SO_Activity_Recid")
opportunityRecId Int? @map("Opportunity_Recid")
assignTo String @map("Assign_To") @db.NVarChar(15)
assignedBy String @map("Assigned_By") @db.NVarChar(15)
companyRecId Int? @map("Company_RecID")
soActivityTypeRecId Int? @map("SO_Activity_Type_RecID")
subject String? @map("Subject") @db.NVarChar(100)
soReferenceRecId Int? @map("SO_Reference_RecID")
dateEntered DateTime @map("Date_Entered") @db.DateTime
enteredBy String @map("Entered_By") @db.NVarChar(15)
contactRecId Int? @map("Contact_RecID")
contactName String? @map("Contact_Name") @db.NVarChar(62)
closeFlag Boolean @map("Close_Flag")
dateClosed DateTime? @map("Date_Closed") @db.DateTime
closedBy String? @map("Closed_By") @db.NVarChar(15)
updatedBy String? @map("Updated_By") @db.NVarChar(15)
lastUpdate DateTime @map("Last_Update") @db.DateTime
notifyCompleteFlag Boolean @map("Notify_Complete_Flag")
notificationSentFlag Boolean @map("Notification_Sent_Flag")
srServiceRecId Int? @map("SR_Service_RecID")
agrHeaderRecId Int? @map("AGR_Header_RecID")
marketingCampaignRecId Int? @map("Marketing_Campaign_RecID")
assignToRecId Int @map("assignto_recid")
assignByRecId Int? @map("assignby_recid")
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
srLocationRecId Int? @map("SR_Location_RecID")
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
automated Boolean @map("Automated")
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime
lastUpdatedUTC DateTime @map("Last_Update_UTC") @db.DateTime
dateClosedUtc DateTime? @map("Date_Closed_UTC") @db.DateTime
soActStatusRecId Int @map("so_act_status_recid")
currencyRecId Int @map("Currency_RecID")
id String? @map("Id") @db.UniqueIdentifier
subject String? @map("Subject") @db.NVarChar(100)
opportunity Opportunity? @relation(fields: [opportunityRecId], references: [opportunityRecId], onDelete: NoAction, onUpdate: NoAction)
soActivityRecId Int @id @map("SO_Activity_Recid")
opportunityRecId Int? @map("Opportunity_Recid")
companyRecId Int? @map("Company_RecID")
assignTo String @map("Assign_To") @db.NVarChar(15)
assignedBy String @map("Assigned_By") @db.NVarChar(15)
soActivityTypeRecId Int? @map("SO_Activity_Type_RecID")
soReferenceRecId Int? @map("SO_Reference_RecID")
dateEntered DateTime @map("Date_Entered") @db.DateTime
enteredBy String @map("Entered_By") @db.NVarChar(15)
contactRecId Int? @map("Contact_RecID")
contactName String? @map("Contact_Name") @db.NVarChar(62)
closeFlag Boolean @map("Close_Flag")
dateClosed DateTime? @map("Date_Closed") @db.DateTime
closedBy String? @map("Closed_By") @db.NVarChar(15)
updatedBy String? @map("Updated_By") @db.NVarChar(15)
lastUpdate DateTime @map("Last_Update") @db.DateTime
notifyCompleteFlag Boolean @map("Notify_Complete_Flag")
notificationSentFlag Boolean @map("Notification_Sent_Flag")
srServiceRecId Int? @map("SR_Service_RecID")
agrHeaderRecId Int? @map("AGR_Header_RecID")
marketingCampaignRecId Int? @map("Marketing_Campaign_RecID")
assignToRecId Int @map("assignto_recid")
assignByRecId Int? @map("assignby_recid")
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
srLocationRecId Int? @map("SR_Location_RecID")
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
automated Boolean @map("Automated")
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime
lastUpdatedUTC DateTime @map("Last_Update_UTC") @db.DateTime
dateClosedUtc DateTime? @map("Date_Closed_UTC") @db.DateTime
soActStatusRecId Int @map("so_act_status_recid")
currencyRecId Int @map("Currency_RecID")
id String? @map("Id") @db.UniqueIdentifier
opportunity Opportunity? @relation(fields: [opportunityRecId], references: [opportunityRecId], onDelete: NoAction, onUpdate: NoAction)
notes SoActivityNotes?
@@map("SO_Activity")
@@schema("dbo")
@@ -2284,83 +2306,84 @@ model ScheduleStatus {
}
model ScheduleType {
scheduleTypeRecId Int @id @map("Schedule_Type_RecID")
tableReference String? @map("Table_Reference") @db.NVarChar(50)
description String? @map("Description") @db.NVarChar(50)
displayColor String? @map("Display_Color") @db.NVarChar(30)
moduleId String? @map("Module_ID") @db.Char(2)
systemFlag Boolean @map("System_Flag")
lastUpdate DateTime @map("Last_Update") @db.DateTime2
updatedBy String? @map("Updated_By") @db.NVarChar(15)
displayFlag Boolean @map("Display_Flag")
xrefMbrTable String? @map("Xref_Mbr_Table") @db.NVarChar(50)
scheduleTypeId String? @map("Schedule_Type_ID") @db.Char(1)
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
srLocationRecId Int? @map("SR_Location_RecID")
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15)
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
id String @map("Id") @db.UniqueIdentifier
scheduleTypeRecId Int @id @map("Schedule_Type_RecID")
tableReference String? @map("Table_Reference") @db.NVarChar(50)
description String? @map("Description") @db.NVarChar(50)
displayColor String? @map("Display_Color") @db.NVarChar(30)
moduleId String? @map("Module_ID") @db.Char(2)
systemFlag Boolean @map("System_Flag")
lastUpdate DateTime @map("Last_Update") @db.DateTime2
updatedBy String? @map("Updated_By") @db.NVarChar(15)
displayFlag Boolean @map("Display_Flag")
xrefMbrTable String? @map("Xref_Mbr_Table") @db.NVarChar(50)
scheduleTypeId String? @map("Schedule_Type_ID") @db.Char(1)
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
srLocationRecId Int? @map("SR_Location_RecID")
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15)
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
id String @map("Id") @db.UniqueIdentifier
schedules Schedule[]
schedules Schedule[]
teChargeCode TeChargeCode? @relation(fields: [teChargeCodeRecId], references: [teChargeCodeRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("Schedule_Type")
@@schema("dbo")
}
model Schedule {
scheduleRecId Int @id @map("Schedule_RecID")
recId Int? @map("RecID")
scheduleTypeRecId Int @map("Schedule_Type_RecID")
memberId String? @map("Member_ID") @db.NVarChar(15)
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
closeFlag Boolean @map("close_flag")
hoursEstimated Decimal? @map("Hours_Estimated") @db.Decimal(18, 2)
lastUpdate DateTime? @map("Last_Update") @db.DateTime
updatedBy String? @map("Updated_By") @db.NVarChar(15)
syncable Boolean @map("Syncable")
lastSync DateTime? @map("Last_Sync") @db.DateTime
exchangeGuid String? @map("Exchange_GUID") @db.VarChar(4000)
reminderFlag Boolean @map("Reminder_Flag")
reminderMinutes Int? @map("Reminder_Minutes")
allDayFlag Boolean @map("All_Day_Flag")
duration Int? @map("Duration")
enteredByRecId Int? @map("Entered_By_RecID")
xrefMbrRecId Int? @map("Xref_Mbr_RecID")
percentSched Int? @map("Percent_Sched")
hoursSched Decimal? @map("Hours_Sched") @db.Decimal(18, 2)
scheduleStatusRecId Int? @map("Schedule_Status_RecID")
hoursPerDay Decimal? @map("Hours_Per_Day") @db.Decimal(18, 2)
ackFlag Boolean? @map("Ack_Flag")
ackMemberRecId Int? @map("Ack_Member_RecID")
ackDate DateTime? @map("Ack_Date") @db.DateTime
closeMemberRecId Int? @map("Close_Member_RecID")
closeDate DateTime? @map("Close_Date") @db.DateTime
billableFlag Boolean? @map("Billable_Flag")
dateEntered DateTime? @map("Date_Entered") @db.DateTime
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
srLocationRecId Int? @map("SR_Location_RecID")
scheduleSpanRecId Int? @map("Schedule_Span_RecID")
meetingFlag Boolean? @map("Meeting_Flag")
recurringFlag Boolean? @map("Recurring_Flag")
ackDateUtc DateTime? @map("Ack_Date_UTC") @db.DateTime
dateEnteredUtc DateTime? @map("Date_Entered_UTC") @db.DateTime
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
closeDateUtc DateTime? @map("Close_Date_UTC") @db.DateTime
enteredBy String? @map("Entered_By") @db.NVarChar(15)
acknowledgedBy String? @map("Acknowledged_By") @db.NVarChar(15)
closedBy String? @map("Closed_By") @db.NVarChar(15)
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
scheduleDesc String? @map("Schedule_Desc") @db.NVarChar(250)
privateFlag Boolean @map("Private_Flag")
notifyType String? @map("NotifyType") @db.NVarChar(2)
scheduleRecId Int @id @map("Schedule_RecID")
recId Int? @map("RecID")
scheduleTypeRecId Int @map("Schedule_Type_RecID")
memberId String? @map("Member_ID") @db.NVarChar(15)
dateTimeStart DateTime? @map("Date_Time_Start") @db.DateTime
dateTimeEnd DateTime? @map("Date_Time_End") @db.DateTime
closeFlag Boolean @map("close_flag")
hoursEstimated Decimal? @map("Hours_Estimated") @db.Decimal(18, 2)
lastUpdate DateTime? @map("Last_Update") @db.DateTime
updatedBy String? @map("Updated_By") @db.NVarChar(15)
syncable Boolean @map("Syncable")
lastSync DateTime? @map("Last_Sync") @db.DateTime
exchangeGuid String? @map("Exchange_GUID") @db.VarChar(4000)
reminderFlag Boolean @map("Reminder_Flag")
reminderMinutes Int? @map("Reminder_Minutes")
allDayFlag Boolean @map("All_Day_Flag")
duration Int? @map("Duration")
enteredByRecId Int? @map("Entered_By_RecID")
xrefMbrRecId Int? @map("Xref_Mbr_RecID")
percentSched Int? @map("Percent_Sched")
hoursSched Decimal? @map("Hours_Sched") @db.Decimal(18, 2)
scheduleStatusRecId Int? @map("Schedule_Status_RecID")
hoursPerDay Decimal? @map("Hours_Per_Day") @db.Decimal(18, 2)
ackFlag Boolean? @map("Ack_Flag")
ackMemberRecId Int? @map("Ack_Member_RecID")
ackDate DateTime? @map("Ack_Date") @db.DateTime
closeMemberRecId Int? @map("Close_Member_RecID")
closeDate DateTime? @map("Close_Date") @db.DateTime
billableFlag Boolean? @map("Billable_Flag")
dateEntered DateTime? @map("Date_Entered") @db.DateTime
mobileGuid String @map("Mobile_Guid") @db.UniqueIdentifier
srLocationRecId Int? @map("SR_Location_RecID")
scheduleSpanRecId Int? @map("Schedule_Span_RecID")
meetingFlag Boolean? @map("Meeting_Flag")
recurringFlag Boolean? @map("Recurring_Flag")
ackDateUtc DateTime? @map("Ack_Date_UTC") @db.DateTime
dateEnteredUtc DateTime? @map("Date_Entered_UTC") @db.DateTime
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
closeDateUtc DateTime? @map("Close_Date_UTC") @db.DateTime
enteredBy String? @map("Entered_By") @db.NVarChar(15)
acknowledgedBy String? @map("Acknowledged_By") @db.NVarChar(15)
closedBy String? @map("Closed_By") @db.NVarChar(15)
dateTimeStartUtc DateTime? @map("Date_Time_Start_UTC") @db.SmallDateTime
dateTimeEndUtc DateTime? @map("Date_Time_End_UTC") @db.SmallDateTime
scheduleDesc String? @map("Schedule_Desc") @db.NVarChar(250)
privateFlag Boolean @map("Private_Flag")
notifyType String? @map("NotifyType") @db.NVarChar(2)
details ScheduleDetail[]
status ScheduleStatus? @relation(fields: [scheduleStatusRecId], references: [scheduleStatusRecId], onDelete: NoAction, onUpdate: NoAction)
type ScheduleType @relation(fields: [scheduleTypeRecId], references: [scheduleTypeRecId], onDelete: NoAction, onUpdate: NoAction)
span ScheduleSpan? @relation(fields: [scheduleSpanRecId], references: [scheduleSpanRecId], onDelete: NoAction, onUpdate: NoAction)
status ScheduleStatus? @relation(fields: [scheduleStatusRecId], references: [scheduleStatusRecId], onDelete: NoAction, onUpdate: NoAction)
type ScheduleType @relation(fields: [scheduleTypeRecId], references: [scheduleTypeRecId], onDelete: NoAction, onUpdate: NoAction)
span ScheduleSpan? @relation(fields: [scheduleSpanRecId], references: [scheduleSpanRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("Schedule")
@@schema("dbo")
@@ -2398,3 +2421,246 @@ model ScheduleDetail {
@@map("Schedule_Detail")
@@schema("dbo")
}
// =====================
// TIME
// =====================
model TimeEntry {
companyRecId Int @map("Company_RecID")
timeRecId Int @map("Time_RecID")
enteredBy String? @map("Entered_By") @db.NVarChar(15)
memberId String? @map("Member_ID") @db.NVarChar(15)
dateStart DateTime? @map("Date_Start") @db.DateTime
timeStart DateTime? @map("Time_Start") @db.DateTime
timeEnd DateTime? @map("Time_End") @db.DateTime
hourlyRate Float? @map("Hourly_Rate") @db.SmallMoney
hoursBill Decimal @map("Hours_Bill") @db.Decimal(18, 2)
invoiceFlag Boolean @map("Invoice_Flag")
lastUpdate DateTime? @map("Last_Update") @db.DateTime
updatedBy String? @map("Updated_By") @db.NVarChar(15)
pmProjectRecId Int? @map("PM_Project_RecID")
hoursActual Decimal @map("Hours_Actual") @db.Decimal(18, 2)
billableFlag Boolean @map("Billable_Flag")
billingLogRecId Int? @map("Billing_Log_RecID")
srServiceRecId Int? @map("SR_Service_RecID")
activityTypeRecId Int? @map("Activity_Type_RecID")
activityClassRecId Int? @map("Activity_Class_RecID")
teStatusId Int @map("TE_Status_ID") @db.SmallInt
timeSheetRecId Int? @map("Time_Sheet_RecID")
teChargeCodeRecId Int? @map("TE_Charge_Code_RecID")
billingUnitRecId Int @map("Billing_Unit_RecID")
ownerLevelRecId Int @map("Owner_Level_RecID")
memberRecId Int? @map("Member_RecID")
hoursInvoiced Decimal? @map("Hours_Invoiced") @db.Decimal(18, 2)
adjustment Decimal @map("Adjustment") @db.Decimal(18, 2)
memberTypeRecId Int? @map("Member_Type_RecID")
agrAmount Decimal @map("Agr_Amount") @db.Decimal(18, 2)
agrHeaderRecId Int? @map("Agr_Header_RecID")
agrAdjustment Decimal? @map("Agr_Adjustment") @db.Decimal(18, 2)
agrHours Decimal? @map("Agr_Hours") @db.Decimal(18, 2)
agrMonth Int? @map("Agr_Month") @db.SmallInt
agrYear Int? @map("Agr_Year") @db.SmallInt
standardRate Decimal? @map("Standard_Rate") @db.Decimal(18, 2)
effectiveRate Decimal? @map("Effective_Rate") @db.Decimal(18, 2)
hoursDeduct Decimal? @map("Hours_Deduct") @db.Decimal(18, 2)
contactRecId Int? @map("Contact_RecID")
soActivityRecId Int? @map("SO_Activity_RecID")
mobileGuid String @map("Mobile_GUID") @db.UniqueIdentifier
billingSr Int? @map("billing_sr")
signatureRecId Int? @map("Signature_RecID")
signatureHours Decimal? @map("Signature_Hours") @db.Decimal(18, 2)
exchangeHref String? @map("exchange_href") @db.NVarChar(300)
teProblemFlag Boolean @map("TE_Problem_Flag")
teResolutionFlag Boolean @map("TE_Resolution_Flag")
teInternalAnalysisFlag Boolean @map("TE_InternalAnalysis_Flag")
documentFlag Boolean @map("Document_Flag")
dateFormat Int? @map("Date_Format")
dbTimestamp Bytes @map("DB_Timestamp") @db.VarBinary(8)
timeStartUtc DateTime? @map("Time_Start_UTC") @db.DateTime
timeEndUtc DateTime? @map("Time_End_UTC") @db.DateTime
lastUpdateUtc DateTime? @map("Last_Update_UTC") @db.DateTime
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime
overageRate Float? @map("Overage_Rate") @db.SmallMoney
extendedInvoiceAmount Decimal? @map("Extended_Invoice_Amount") @db.Decimal(31, 6)
extendedBillAmount Decimal? @map("Extended_Bill_Amount") @db.Decimal(31, 6)
notificationHistory String @map("Notification_History") @db.VarChar(4000)
mergedFlag Boolean @map("Merged_Flag")
internalNote String? @map("Internal_Note") @db.NVarChar(Max)
reference String? @map("Reference") @db.NVarChar(100)
originalAuthor String? @map("Original_Author") @db.NVarChar(150)
costPerHour String @map("Cost_Per_Hour") @db.NVarChar(2000)
overrideFlag Boolean @map("Override_Flag")
issueFlag Boolean @map("Issue_Flag")
notes String? @map("Notes") @db.NVarChar(4000)
notesMarkdown String? @map("Notes_Markdown") @db.NVarChar(Max)
chargeToRecId Int? @map("Charge_To_RecID")
chargeToType String? @map("Charge_To_Type") @db.NVarChar(13)
teChargeCode TeChargeCode? @relation(fields: [teChargeCodeRecId], references: [teChargeCodeRecId], onDelete: NoAction, onUpdate: NoAction)
memberType MemberType? @relation(fields: [memberTypeRecId], references: [memberTypeRecId], onDelete: NoAction, onUpdate: NoAction)
@@id([companyRecId, timeRecId])
@@map("Time_Entry")
@@schema("dbo")
}
model SoActivityType {
soActivityTypeRecId Int @id @map("SO_Activity_Type_RecID")
soActivityTypeId String? @map("SO_Activity_Type_ID") @db.NVarChar(15)
description String? @map("Description") @db.NVarChar(50)
historyFlag Boolean @map("History_Flag")
updatedBy String @map("Updated_By") @db.NVarChar(15)
cleanUpFlag Boolean @map("CleanUp_Flag")
defaultFlag Boolean @map("Default_Flag")
importFlag Boolean @map("Import_Flag")
emailFlag Boolean @map("email_flag")
memoFlag Boolean? @map("Memo_Flag")
pointsValue Int? @map("Points_Value")
inactiveFlag Boolean? @map("Inactive_Flag")
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15)
id String @map("Id") @db.UniqueIdentifier
@@map("SO_Activity_Type")
@@schema("dbo")
}
model SoActStatus {
soActStatusRecId Int @id @map("SO_Act_Status_RecID")
description String? @map("Description") @db.NVarChar(30)
defaultFlag Boolean @map("Default_Flag")
closedFlag Boolean @map("Closed_Flag")
inactiveFlag Boolean @map("Inactive_Flag")
spawnFollowupFlag Boolean @map("spawn_followup_flag")
lastUpdate DateTime @map("Last_Update") @db.DateTime2
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
updatedBy String? @map("Updated_By") @db.NVarChar(15)
enteredBy String @map("Entered_By") @db.NVarChar(15)
id String @map("Id") @db.UniqueIdentifier
@@map("SO_Act_Status")
@@schema("dbo")
}
model SoActivityNotes {
soActivityNotesRecId Int @id @map("SO_Activity_Notes_RecID")
soActivityRecId Int @unique @map("SO_Activity_RecID")
notes String @map("Notes") @db.NVarChar(Max)
internalAnalysisFlag Boolean @map("Internal_Analysis_Flag")
dateCreatedUtc DateTime @map("Date_Created_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15)
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
updatedBy String @map("Updated_By") @db.NVarChar(15)
soActivity SoActivity @relation(fields: [soActivityRecId], references: [soActivityRecId], onDelete: NoAction, onUpdate: NoAction)
@@map("SO_Activity_Notes")
@@schema("dbo")
}
// =====================
// TIME ENTRY LOOKUPS
// =====================
model TeStatus {
teStatusRecId Int @id @map("TE_Status_RecID")
teStatusId Int @map("TE_Status_ID") @db.SmallInt
description String? @map("Description") @db.NVarChar(50)
action String? @map("Action") @db.NVarChar(50)
lastUpdatedUtc DateTime @map("Last_Updated_UTC") @db.DateTime2
localeKeyRecId Int? @map("Locale_Key_RecID")
@@map("TE_Status")
@@schema("dbo")
}
model TeChargeCode {
teChargeCodeRecId Int @id @map("TE_Charge_Code_RecID")
description String? @map("Description") @db.NVarChar(50)
companyRecId Int? @map("Company_RecID")
ownerLevelRecId Int? @map("Owner_Level_RecID")
billingUnitRecId Int? @map("Billing_Unit_RecID")
activityClassRecId Int? @map("Activity_Class_RecID")
activityTypeRecId Int? @map("Activity_Type_RecID")
expenseFlag Boolean @map("Expense_Flag")
timeFlag Boolean @map("Time_Flag")
updatedBy String? @map("Updated_By") @db.NVarChar(15)
lastUpdate DateTime @map("Last_Update") @db.DateTime2
billableFlag Boolean @map("Billable_Flag")
exTypeFlag Boolean @map("EX_Type_Flag")
invoiceFlag Boolean? @map("Invoice_Flag")
integrationXref String? @map("Integration_Xref") @db.NVarChar(50)
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15)
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
id String @map("Id") @db.UniqueIdentifier
scheduleTypes ScheduleType[]
timeEntries TimeEntry[]
@@map("TE_Charge_Code")
@@schema("dbo")
}
model MemberType {
memberTypeRecId Int @id @map("Member_Type_RecID")
description String? @map("Description") @db.NVarChar(30)
inactiveFlag Boolean @map("Inactive_Flag")
updatedBy String? @map("Updated_By") @db.NVarChar(15)
lastUpdate DateTime @map("Last_Update") @db.DateTime2
lastUpdateUtc DateTime @map("Last_Update_UTC") @db.DateTime2
enteredBy String @map("Entered_By") @db.NVarChar(15)
dateEnteredUtc DateTime @map("Date_Entered_UTC") @db.DateTime2
id String @map("Id") @db.UniqueIdentifier
members Member[]
timeEntries TimeEntry[]
@@map("Member_Type")
@@schema("dbo")
}
+10
View File
@@ -31,6 +31,16 @@ export { scheduleTypeTranslation } from "./translations/schedule-type";
export { scheduleSpanTranslation } from "./translations/schedule-span";
export { scheduleTranslation } from "./translations/schedule";
export { taxCodeTranslation } from "./translations/tax-code";
export { timeEntryTranslation } from "./translations/time-entry";
export { timeEntryStatusTranslation } from "./translations/time-entry-status";
export { timeEntryChargeCodeTranslation } from "./translations/time-entry-charge-code";
export { timeActivityClassTranslation } from "./translations/time-activity-class";
export { timeActivityTypeTranslation } from "./translations/time-activity-type";
export { activityTypeTranslation } from "./translations/activity-type";
export { activityStatusTranslation } from "./translations/activity-status";
export { activityTranslation } from "./translations/activity";
export { activityNotesTranslation } from "./translations/activity-notes";
export { cwMemberTypeTranslation } from "./translations/cw-member-type";
// Context type exports
export type { TranslationContext } from "./translations/context";
+77 -4
View File
@@ -37,6 +37,12 @@ import {
scheduleTypeTranslation,
scheduleSpanTranslation,
scheduleTranslation,
taxCodeTranslation,
timeEntryTranslation,
activityTypeTranslation,
activityStatusTranslation,
activityTranslation,
activityNotesTranslation,
userTranslation,
warehouseBinTranslation,
type TranslationContext,
@@ -203,6 +209,9 @@ const refreshContextFromApi = async (
context.scheduleStatusIds.clear();
context.scheduleTypeIds.clear();
context.scheduleSpanIds.clear();
context.activityTypeIds.clear();
context.activityStatusIds.clear();
context.activityIds.clear();
const [
users,
@@ -215,6 +224,9 @@ const refreshContextFromApi = async (
scheduleStatuses,
scheduleTypes,
scheduleSpans,
activityTypes,
activityStatuses,
activities,
] = await Promise.all([
apiPrisma.user.findMany({
select: {
@@ -269,6 +281,21 @@ const refreshContextFromApi = async (
id: true,
},
}),
apiPrisma.activityType.findMany({
select: {
id: true,
},
}),
apiPrisma.activityStatus.findMany({
select: {
id: true,
},
}),
apiPrisma.activity.findMany({
select: {
id: true,
},
}),
]);
const defaultOpportunityType = await apiPrisma.opportunityType.findFirst({
@@ -346,6 +373,18 @@ const refreshContextFromApi = async (
context.scheduleSpanIds.add(span.id);
}
for (const activityType of activityTypes) {
context.activityTypeIds.add(activityType.id);
}
for (const activityStatus of activityStatuses) {
context.activityStatusIds.add(activityStatus.id);
}
for (const activity of activities) {
context.activityIds.add(activity.id);
}
context.defaultOpportunityTypeId = defaultOpportunityType?.id ?? null;
// Optional context fed from env-provided maps.
@@ -813,6 +852,41 @@ const getConfigForTable = (table: string): SyncTableConfig | null => {
uniqueField: "id",
lastUpdatedField: "lastUpdateUtc",
},
timeEntry: {
sourceModel: "timeEntry",
targetModel: "timeEntry",
translation: timeEntryTranslation as unknown as AnyTranslation,
uniqueField: "id",
lastUpdatedField: "lastUpdateUtc",
},
soActivityType: {
sourceModel: "soActivityType",
targetModel: "activityType",
translation: activityTypeTranslation as unknown as AnyTranslation,
uniqueField: "id",
lastUpdatedField: "lastUpdateUtc",
},
soActStatus: {
sourceModel: "soActStatus",
targetModel: "activityStatus",
translation: activityStatusTranslation as unknown as AnyTranslation,
uniqueField: "id",
lastUpdatedField: "lastUpdateUtc",
},
soActivity: {
sourceModel: "soActivity",
targetModel: "activity",
translation: activityTranslation as unknown as AnyTranslation,
uniqueField: "id",
lastUpdatedField: "lastUpdatedUTC",
},
soActivityNotes: {
sourceModel: "soActivityNotes",
targetModel: "activityNotes",
translation: activityNotesTranslation as unknown as AnyTranslation,
uniqueField: "id",
lastUpdatedField: "lastUpdateUtc",
},
};
return configMap[table] ?? null;
@@ -972,8 +1046,8 @@ export async function syncTableUpdates(
uniqueFields.length > 0
? formatUniqueConstraintError(error, translatedData)
: error instanceof Error
? error.message
: "Unknown row sync error";
? error.message
: "Unknown row sync error";
console.error(
`Failed row in ${config.sourceModel} -> ${config.targetModel}:`,
message
@@ -984,8 +1058,7 @@ export async function syncTableUpdates(
if (failed > sampleErrorsPrinted) {
console.error(
`${config.sourceModel}: suppressed ${
failed - sampleErrorsPrinted
`${config.sourceModel}: suppressed ${failed - sampleErrorsPrinted
} additional row errors`
);
}
+189 -32
View File
@@ -39,6 +39,16 @@ import {
scheduleSpanTranslation,
scheduleTranslation,
taxCodeTranslation,
timeEntryTranslation,
timeEntryStatusTranslation,
timeEntryChargeCodeTranslation,
timeActivityClassTranslation,
timeActivityTypeTranslation,
activityTypeTranslation,
activityStatusTranslation,
activityTranslation,
activityNotesTranslation,
cwMemberTypeTranslation,
userTranslation,
warehouseBinTranslation,
type TranslationContext,
@@ -114,7 +124,7 @@ const CRITICAL_CW_WATERMARK_OVERLAP_MS =
const criticalCwDeltaLimit = Math.max(
100,
Number.parseInt(process.env.DALPURI_CRITICAL_CW_DELTA_LIMIT ?? "5000", 10) ||
5000
5000
);
const lastCriticalFullSyncByStep = new Map<string, number>();
@@ -346,6 +356,11 @@ const refreshContextFromApi = async (
context.scheduleTypeIds.clear();
context.scheduleSpanIds.clear();
context.taxCodeIds.clear();
context.activityTypeIds.clear();
context.activityStatusIds.clear();
context.activityIds.clear();
context.timeEntryChargeCodeIds.clear();
context.timeEntryStatusIdByTeStatusId.clear();
const [
users,
@@ -363,6 +378,11 @@ const refreshContextFromApi = async (
scheduleTypes,
scheduleSpans,
taxCodes,
activityTypes,
activityStatuses,
activities,
timeEntryChargeCodes,
timeEntryStatuses,
] = await Promise.all([
apiPrisma.user.findMany({
select: {
@@ -442,6 +462,32 @@ const refreshContextFromApi = async (
id: true,
},
}),
apiPrisma.activityType.findMany({
select: {
id: true,
},
}),
apiPrisma.activityStatus.findMany({
select: {
id: true,
},
}),
apiPrisma.activity.findMany({
select: {
id: true,
},
}),
apiPrisma.timeEntryChargeCode.findMany({
select: {
id: true,
},
}),
apiPrisma.timeEntryStatus.findMany({
select: {
id: true,
statusId: true,
},
}),
]);
const defaultOpportunityType = await apiPrisma.opportunityType.findFirst({
@@ -539,6 +585,26 @@ const refreshContextFromApi = async (
context.taxCodeIds.add(taxCode.id);
}
for (const activityType of activityTypes) {
context.activityTypeIds.add(activityType.id);
}
for (const activityStatus of activityStatuses) {
context.activityStatusIds.add(activityStatus.id);
}
for (const activity of activities) {
context.activityIds.add(activity.id);
}
for (const chargeCode of timeEntryChargeCodes) {
context.timeEntryChargeCodeIds.add(chargeCode.id);
}
for (const status of timeEntryStatuses) {
context.timeEntryStatusIdByTeStatusId.set(status.statusId, status.id);
}
context.defaultOpportunityTypeId = defaultOpportunityType?.id ?? null;
// Optional context fed from env-provided maps.
@@ -645,17 +711,17 @@ const sanitizeUserForeignKeys = (
targetModel === "serviceTicketNote"
? ["authorId"]
: targetModel === "serviceTicket"
? ["createdById", "updatedById", "closedById"]
: [];
? ["createdById", "updatedById", "closedById"]
: [];
const identifierFields =
targetModel === "serviceTicket"
? ["ticketOwnerId"]
: targetModel === "company"
? ["enteredById", "deletedById"]
: targetModel === "companyAddress"
? ["updatedById"]
: [];
? ["enteredById", "deletedById"]
: targetModel === "companyAddress"
? ["updatedById"]
: [];
for (const field of userIdFields) {
const value = sanitized[field];
@@ -1073,8 +1139,7 @@ const reconcileStepDeletes = async (
const message =
error instanceof Error ? error.message : "Unknown delete error";
console.error(
`Delete reconcile failed in ${step.name} for ${
step.uniqueField
`Delete reconcile failed in ${step.name} for ${step.uniqueField
}=${formatValue(value)}:`,
message
);
@@ -1085,8 +1150,7 @@ const reconcileStepDeletes = async (
if (failed > sampleErrorsPrinted) {
console.error(
`${step.name}: suppressed ${
failed - sampleErrorsPrinted
`${step.name}: suppressed ${failed - sampleErrorsPrinted
} additional delete errors`
);
}
@@ -1097,10 +1161,10 @@ const reconcileStepDeletes = async (
type SmartSyncDecision =
| { mode: "full"; differences: SmartSyncDifference[] }
| {
mode: "incremental";
sourceIds: number[];
differences: SmartSyncDifference[];
};
mode: "incremental";
sourceIds: number[];
differences: SmartSyncDifference[];
};
type SmartSyncDifference = {
sourceId: number;
@@ -1129,10 +1193,8 @@ const logAllSmartSyncDifferences = (
? diff.apiUpdatedAt.toISOString()
: "null";
console.log(
` [diff] sourceModel=${step.sourceModel} targetModel=${
step.targetModel
} id=${diff.sourceId} reason=${
diff.reason
` [diff] sourceModel=${step.sourceModel} targetModel=${step.targetModel
} id=${diff.sourceId} reason=${diff.reason
} cwUpdatedAt=${diff.cwUpdatedAt.toISOString()} apiUpdatedAt=${apiUpdated}`
);
}
@@ -1347,8 +1409,8 @@ const syncStep = async (
uniqueFields.length > 0
? formatUniqueConstraintError(error, translatedData)
: error instanceof Error
? error.message
: "Unknown row sync error";
? error.message
: "Unknown row sync error";
console.error(
`Failed row in ${step.name} (source ${step.sourceModel} -> target ${step.targetModel}):`,
message
@@ -1393,8 +1455,7 @@ const syncStep = async (
if (failed > sampleErrorsPrinted) {
console.error(
`${step.name}: suppressed ${
failed - sampleErrorsPrinted
`${step.name}: suppressed ${failed - sampleErrorsPrinted
} additional row errors`
);
}
@@ -1495,6 +1556,15 @@ export const executeFullDalpuriSync = async (options?: {
const isTimedOut = () => Date.now() - syncStartTime > timeoutMs;
const steps: Step[] = [
{
name: "CW Member Types",
sourceModel: "memberType",
targetModel: "cwMemberType",
translation: cwMemberTypeTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "memberTypeRecId",
sourceUpdatedField: "lastUpdateUtc",
},
{
name: "CW Members",
sourceModel: "member",
@@ -1829,6 +1899,87 @@ export const executeFullDalpuriSync = async (options?: {
sourceIdField: "scheduleRecId",
sourceUpdatedField: "lastUpdateUtc",
},
{
name: "Time Entry Statuses",
sourceModel: "teStatus",
targetModel: "timeEntryStatus",
translation: timeEntryStatusTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "teStatusRecId",
sourceUpdatedField: "lastUpdatedUtc",
},
{
name: "Time Entry Charge Codes",
sourceModel: "teChargeCode",
targetModel: "timeEntryChargeCode",
translation: timeEntryChargeCodeTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "teChargeCodeRecId",
sourceUpdatedField: "lastUpdateUtc",
},
{
name: "Time Activity Classes",
sourceModel: "activityClass",
targetModel: "timeActivityClass",
translation: timeActivityClassTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "activityClassRecId",
sourceUpdatedField: "lastUpdatedUtc",
},
{
name: "Time Activity Types",
sourceModel: "activityType",
targetModel: "timeActivityType",
translation: timeActivityTypeTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "activityTypeRecId",
sourceUpdatedField: "lastUpdatedUtc",
},
{
name: "Time Entries",
sourceModel: "timeEntry",
targetModel: "timeEntry",
translation: timeEntryTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "timeRecId",
sourceUpdatedField: "lastUpdateUtc",
},
{
name: "Activity Types",
sourceModel: "soActivityType",
targetModel: "activityType",
translation: activityTypeTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "soActivityTypeRecId",
sourceUpdatedField: "lastUpdateUtc",
},
{
name: "Activity Statuses",
sourceModel: "soActStatus",
targetModel: "activityStatus",
translation: activityStatusTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "soActStatusRecId",
sourceUpdatedField: "lastUpdateUtc",
},
{
name: "Activities",
sourceModel: "soActivity",
targetModel: "activity",
translation: activityTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "soActivityRecId",
sourceUpdatedField: "lastUpdatedUTC",
},
{
name: "Activity Notes",
sourceModel: "soActivityNotes",
targetModel: "activityNotes",
translation: activityNotesTranslation as unknown as AnyTranslation,
uniqueField: "id",
sourceIdField: "soActivityNotesRecId",
sourceUpdatedField: "lastUpdateUtc",
},
];
try {
@@ -1875,7 +2026,16 @@ export const executeFullDalpuriSync = async (options?: {
step.targetModel === "opportunity" ||
step.targetModel === "productData" ||
step.targetModel === "serviceTicketNote" ||
step.targetModel === "schedule"
step.targetModel === "schedule" ||
step.targetModel === "timeEntryStatus" ||
step.targetModel === "timeEntryChargeCode" ||
step.targetModel === "timeActivityClass" ||
step.targetModel === "timeActivityType" ||
step.targetModel === "timeEntry" ||
step.targetModel === "activityType" ||
step.targetModel === "activityStatus" ||
step.targetModel === "activity" ||
step.targetModel === "activityNotes"
) {
try {
await refreshContextFromApi(apiPrisma, context);
@@ -1925,12 +2085,10 @@ export const executeFullDalpuriSync = async (options?: {
? effectiveDecision.sourceIds
: undefined;
console.log(
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${
effectiveDecision.mode
}${
effectiveDecision.mode === "incremental"
? ` (${effectiveDecision.sourceIds.length} ids)`
: ""
` [smart-sync]${forceIncremental ? "[forced]" : ""} mode=${effectiveDecision.mode
}${effectiveDecision.mode === "incremental"
? ` (${effectiveDecision.sourceIds.length} ids)`
: ""
}`
);
if (logAllDifferences) {
@@ -1978,8 +2136,7 @@ export const executeFullDalpuriSync = async (options?: {
if (forceIncremental) {
const selected = deleteSteps[0];
console.log(
`[delete-reconcile] incremental sweep: ${selected.name} (${
incrementalDeleteStepIndex + 1
`[delete-reconcile] incremental sweep: ${selected.name} (${incrementalDeleteStepIndex + 1
}/${steps.length})`
);
incrementalDeleteStepIndex =
@@ -0,0 +1,48 @@
import { SoActivityNotes as CwSoActivityNotes } from "../../generated/prisma/client";
import { Translation, skipRow } from "./types";
import { TranslationContext } from "./context";
type ApiActivityNotesRecord = {
id: number;
notes: string;
activityId: number | null;
internalAnalysisFlag: boolean;
enteredById: string | null;
updatedById: string | null;
createdAt: Date;
updatedAt: Date;
};
export const activityNotesTranslation: Translation<
CwSoActivityNotes,
ApiActivityNotesRecord,
TranslationContext
> = {
values: [
{ from: "soActivityNotesRecId", to: "id" },
{
from: "notes",
to: "notes",
process: (value: string) => value,
},
{
from: "soActivityRecId",
to: "activityId",
process: (value: number, context: TranslationContext) => {
if (!context.activityIds.has(value)) {
skipRow(`Activity ${value} not found in API`);
}
return value;
},
},
{
from: "internalAnalysisFlag",
to: "internalAnalysisFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "enteredById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateCreatedUtc", to: "createdAt" },
{ from: "lastUpdateUtc", to: "updatedAt" },
],
};
@@ -0,0 +1,55 @@
import { SoActStatus as CwSoActStatus } from "../../generated/prisma/client";
import { Translation } from "./types";
type ApiActivityStatusRecord = {
id: number;
name: string;
description: string | null;
closedFlag: boolean;
inactiveFlag: boolean;
defaultFlag: boolean;
spawnFollowupFlag: boolean;
createdById: string | null;
updatedById: string | null;
createdAt: Date;
updatedAt: Date;
};
export const activityStatusTranslation: Translation<
CwSoActStatus,
ApiActivityStatusRecord
> = {
values: [
{ from: "soActStatusRecId", to: "id" },
{
from: "description",
to: "name",
process: (value) => (value ? value : "Unknown Status"),
},
{ from: "description", to: "description" },
{
from: "closedFlag",
to: "closedFlag",
process: (value) => Boolean(value),
},
{
from: "inactiveFlag",
to: "inactiveFlag",
process: (value) => Boolean(value),
},
{
from: "defaultFlag",
to: "defaultFlag",
process: (value) => Boolean(value),
},
{
from: "spawnFollowupFlag",
to: "spawnFollowupFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{ from: "dateEnteredUtc", to: "createdAt" },
{ from: "lastUpdateUtc", to: "updatedAt" },
],
};
+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" },
],
};
+20
View File
@@ -73,6 +73,21 @@ export interface TranslationContext {
// Set of API TaxCode.id values for FK validation
taxCodeIds: Set<number>;
// Set of API ActivityType.id values for FK validation
activityTypeIds: Set<number>;
// Set of API ActivityStatus.id values for FK validation
activityStatusIds: Set<number>;
// Set of API Activity.id values for FK validation (used by ActivityNotes)
activityIds: Set<number>;
// Set of API TimeEntryChargeCode.id values for FK validation
timeEntryChargeCodeIds: Set<number>;
// Map: CW TE_Status_ID (smallint) -> API TimeEntryStatus.id (rec ID)
timeEntryStatusIdByTeStatusId: Map<number, number>;
}
/**
@@ -103,5 +118,10 @@ export function createTranslationContext(): TranslationContext {
scheduleTypeIds: new Set(),
scheduleSpanIds: new Set(),
taxCodeIds: new Set(),
activityTypeIds: new Set(),
activityStatusIds: new Set(),
activityIds: new Set(),
timeEntryChargeCodeIds: new Set(),
timeEntryStatusIdByTeStatusId: new Map(),
};
}
@@ -0,0 +1,39 @@
import { MemberType as CwMemberType } from "../../generated/prisma/client";
import { Translation } from "./types";
type ApiCwMemberTypeRecord = {
id: number;
description: string | null;
inactiveFlag: boolean;
createdById: string | null;
updatedById: string | null;
createdAt: Date;
updatedAt: Date;
};
export const cwMemberTypeTranslation: Translation<
CwMemberType,
ApiCwMemberTypeRecord
> = {
values: [
{ from: "memberTypeRecId", to: "id" },
{ from: "description", to: "description" },
{
from: "inactiveFlag",
to: "inactiveFlag",
process: (value) => Boolean(value),
},
{ from: "enteredBy", to: "createdById" },
{ from: "updatedBy", to: "updatedById" },
{
from: "dateEnteredUtc",
to: "createdAt",
process: (value) => (value as Date | null) ?? new Date(0),
},
{
from: "lastUpdateUtc",
to: "updatedAt",
process: (value) => (value as Date | null) ?? new Date(0),
},
],
};
@@ -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),
},
],
};