- Add source, title, mobilePhone fields to ClientContact schema - Add composite unique constraint for dedup (clientId, name, label, source) - Add fetchAfwContacts() query with dedup by (CustId, Name, Responsibility) - Add syncContacts() phase to sync engine with retry logic - Update contacts UI to show title, mobile phone, AMS badge - Disable edit/delete on AMS-synced contacts (source='ams')
463 lines
12 KiB
TypeScript
463 lines
12 KiB
TypeScript
import { executeAfwQuery } from './afw-connection'
|
|
|
|
/**
|
|
* AFW Customer data structure
|
|
*/
|
|
export interface AfwCustomer {
|
|
CustId: string
|
|
FirmNameCust: string | null
|
|
LastName: string | null
|
|
FirstName: string | null
|
|
Addr1: string | null
|
|
Addr2: string | null
|
|
City: string | null
|
|
State: string | null
|
|
ZipCode: string | null
|
|
BusPhone: string | null
|
|
EMail: string | null
|
|
Prod1Code: string | null
|
|
ChangedDate: Date | null
|
|
}
|
|
|
|
/**
|
|
* AFW Policy data structure (enriched with lookups)
|
|
*/
|
|
export interface AfwPolicy {
|
|
PolId: string
|
|
FirmNameCust: string | null
|
|
CustNo: number | null
|
|
PolNo: string | null
|
|
PolEffDate: Date | null
|
|
PolExpDate: Date
|
|
ParentCompanyName: string | null
|
|
WritingCompanyName: string | null
|
|
ExecFormattedName: string | null
|
|
CsrFormattedName: string | null
|
|
Status: string | null
|
|
PolTypeLOB: string | null
|
|
BillMethod: string | null
|
|
BusinessType: string | null
|
|
PolicyDepartment: string | null
|
|
AddRep1FormattedName: string | null
|
|
AddRep2FormattedName: string | null
|
|
AddExec1FormattedName: string | null
|
|
AddExec2FormattedName: string | null
|
|
ANotId: string | null
|
|
CustId: string
|
|
}
|
|
|
|
/**
|
|
* AFW Employee data structure
|
|
*/
|
|
export interface AfwEmployee {
|
|
EmpCode: string
|
|
LastName: string
|
|
FirstName: string
|
|
MiddleName: string | null
|
|
ShortName: string | null
|
|
Title: string | null
|
|
EMail: string | null
|
|
Status: string
|
|
IsRep: string | null
|
|
IsProd: string | null
|
|
EmpSupervisorCode: string | null
|
|
DefaultGLDeptCode: string | null
|
|
}
|
|
|
|
/**
|
|
* AFW General Ledger Department data structure
|
|
*/
|
|
export interface AfwGLDepartment {
|
|
GLDeptCode: string
|
|
Name: string
|
|
ShortName: string | null
|
|
}
|
|
|
|
/**
|
|
* AFW Company data structure
|
|
*/
|
|
export interface AfwCompany {
|
|
CompanyCode: string
|
|
CompanyName: string
|
|
Address: string | null
|
|
}
|
|
|
|
/**
|
|
* AFW PR Code data structure
|
|
*/
|
|
export interface AfwPRCode {
|
|
PRCode: string
|
|
PRName: string
|
|
Description: string | null
|
|
}
|
|
|
|
/**
|
|
* AFW Policy Contact data structure (deduplicated per customer)
|
|
*/
|
|
export interface AfwContact {
|
|
CustId: string
|
|
Name: string
|
|
Responsibility: string | null
|
|
Title: string | null
|
|
AreaCode: string | null
|
|
Phone: string | null
|
|
MobileAreaCode: string | null
|
|
MobilePhone: string | null
|
|
EMail: string | null
|
|
Notes: string | null
|
|
}
|
|
|
|
/**
|
|
* Fetch deduplicated contacts from AFW_PolContact, joined to customers.
|
|
* Deduplicates by (CustId, Name, Responsibility) keeping the most recently entered.
|
|
*/
|
|
export async function fetchAfwContacts(): Promise<AfwContact[]> {
|
|
const query = `
|
|
WITH ranked AS (
|
|
SELECT
|
|
c.CustId,
|
|
pc.Name,
|
|
pc.Responsibility,
|
|
pc.Title,
|
|
pc.AreaCode,
|
|
pc.Phone,
|
|
pc.MobileAreaCode,
|
|
pc.MobilePhone,
|
|
pc.EMail,
|
|
pc.Notes,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY c.CustId, pc.Name, pc.Responsibility
|
|
ORDER BY pc.EnteredDate DESC
|
|
) AS rn
|
|
FROM AFW_PolContact pc
|
|
INNER JOIN AFW_BasicPolInfo p ON p.PolId = pc.PolId
|
|
INNER JOIN AFW_Customer c ON c.CustId = p.CustId
|
|
WHERE pc.Status = 'A'
|
|
AND pc.Name IS NOT NULL
|
|
AND pc.Name != ''
|
|
)
|
|
SELECT CustId, Name, Responsibility, Title, AreaCode, Phone,
|
|
MobileAreaCode, MobilePhone, EMail, Notes
|
|
FROM ranked
|
|
WHERE rn = 1
|
|
ORDER BY CustId, Responsibility, Name
|
|
`
|
|
|
|
return executeAfwQuery<AfwContact>(query)
|
|
}
|
|
|
|
/**
|
|
* Fetch customers from AFW database
|
|
*/
|
|
export async function fetchAfwCustomers(
|
|
modifiedSince?: Date
|
|
): Promise<AfwCustomer[]> {
|
|
let query = `
|
|
SELECT
|
|
CustId,
|
|
FirmNameCust,
|
|
LastName,
|
|
FirstName,
|
|
Addr1,
|
|
Addr2,
|
|
City,
|
|
State,
|
|
ZipCode,
|
|
BusPhone,
|
|
EMail,
|
|
Prod1Code,
|
|
ChangedDate
|
|
FROM AFW_Customer
|
|
WHERE Active = 'Y'
|
|
`
|
|
|
|
if (modifiedSince) {
|
|
query += ` AND ChangedDate > @modifiedSince`
|
|
}
|
|
|
|
query += ` ORDER BY ChangedDate DESC`
|
|
|
|
return executeAfwQuery<AfwCustomer>(query, modifiedSince ? { modifiedSince } : undefined)
|
|
}
|
|
|
|
/**
|
|
* Fetch policies from AFW database with comprehensive lookups and personnel
|
|
* Based on the AFW_PolicyBizVw query structure
|
|
*/
|
|
export async function fetchAfwPolicies(
|
|
startDate?: Date,
|
|
endDate?: Date,
|
|
modifiedSince?: Date
|
|
): Promise<AfwPolicy[]> {
|
|
// Default to 2026 policies only
|
|
const policyStartDate = startDate || new Date('2026-01-01')
|
|
const policyEndDate = endDate || new Date('2026-12-31')
|
|
|
|
const query = `
|
|
WITH addpersonnel AS (
|
|
SELECT
|
|
PolId,
|
|
PolPId,
|
|
AFW_Employee.EmpCode,
|
|
EmpType,
|
|
AFW_Employee.LastName + ', ' + AFW_Employee.FirstName AS Name,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY PolId, EmpType
|
|
ORDER BY AFW_PolicyPersonnel.EnteredDate DESC
|
|
) AS rn
|
|
FROM dbo.AFW_PolicyPersonnel
|
|
INNER JOIN AFW_Employee
|
|
ON AFW_Employee.EmpCode = AFW_PolicyPersonnel.EmpCode
|
|
WHERE IsPrimary = 'N'
|
|
)
|
|
SELECT
|
|
AFW_BasicPolInfo.PolId,
|
|
AFW_BasicPolInfo.CustId,
|
|
AFW_Customer.FirmNameCust,
|
|
AFW_Customer.CustNo,
|
|
AFW_BasicPolInfo.PolNo,
|
|
AFW_BasicPolInfo.PolEffDate,
|
|
AFW_BasicPolInfo.PolExpDate,
|
|
ParentCompany.Name AS ParentCompanyName,
|
|
WritingCompany.Name AS WritingCompanyName,
|
|
ExecCode.LastName + ', ' + ExecCode.FirstName AS ExecFormattedName,
|
|
CsrCode.LastName + ', ' + CsrCode.FirstName AS CsrFormattedName,
|
|
RenewalRptFlag.Description AS Status,
|
|
AFW_BasicPolInfo.PolTypeLOB,
|
|
BillMethods.Description AS BillMethod,
|
|
TypeOfBusiness.Description AS BusinessType,
|
|
AFW_GeneralLedgerDepartment.Name AS PolicyDepartment,
|
|
addpersonnel1.Name AS AddRep1FormattedName,
|
|
addpersonnel2.Name AS AddRep2FormattedName,
|
|
addpersonnel3.Name AS AddExec1FormattedName,
|
|
addpersonnel4.Name AS AddExec2FormattedName,
|
|
AFW_Customer.ANotId
|
|
FROM dbo.AFW_BasicPolInfo
|
|
INNER JOIN AFW_Customer
|
|
ON AFW_Customer.CustId = AFW_BasicPolInfo.CustId
|
|
INNER JOIN AFW_Company AS ParentCompany
|
|
ON ParentCompany.CoCode = AFW_BasicPolInfo.CoCode
|
|
INNER JOIN AFW_Company AS WritingCompany
|
|
ON WritingCompany.CoCode = AFW_BasicPolInfo.WritingCoCode
|
|
INNER JOIN AFW_GeneralLedgerDepartment
|
|
ON AFW_GeneralLedgerDepartment.GLDeptCode = AFW_BasicPolInfo.GLDeptCode
|
|
INNER JOIN AFW_PRCode AS TypeOfBusiness
|
|
ON TypeOfBusiness.Code = AFW_BasicPolInfo.TypeOfBus
|
|
AND TypeOfBusiness.AttrCode = 'TB'
|
|
INNER JOIN AFW_PRCode AS BillMethods
|
|
ON BillMethods.Code = AFW_BasicPolInfo.BillMethod
|
|
AND BillMethods.AttrCode = 'BM'
|
|
INNER JOIN AFW_PRCode AS RenewalRptFlag
|
|
ON RenewalRptFlag.Code = AFW_BasicPolInfo.RenewalRptFlag
|
|
AND RenewalRptFlag.AttrCode = 'PF'
|
|
INNER JOIN AFW_Employee AS ExecCode
|
|
ON ExecCode.EmpCode = AFW_BasicPolInfo.ExecCode
|
|
INNER JOIN AFW_Employee AS CsrCode
|
|
ON CsrCode.EmpCode = AFW_BasicPolInfo.CsrCode
|
|
LEFT JOIN addpersonnel AS addpersonnel1
|
|
ON addpersonnel1.PolId = AFW_BasicPolInfo.PolId
|
|
AND addpersonnel1.rn = 1
|
|
AND addpersonnel1.EmpType = 'R'
|
|
LEFT JOIN addpersonnel AS addpersonnel2
|
|
ON addpersonnel2.PolId = AFW_BasicPolInfo.PolId
|
|
AND addpersonnel2.rn = 2
|
|
AND addpersonnel2.EmpType = 'R'
|
|
LEFT JOIN addpersonnel AS addpersonnel3
|
|
ON addpersonnel3.PolId = AFW_BasicPolInfo.PolId
|
|
AND addpersonnel3.rn = 1
|
|
AND addpersonnel3.EmpType = 'P'
|
|
LEFT JOIN addpersonnel AS addpersonnel4
|
|
ON addpersonnel4.PolId = AFW_BasicPolInfo.PolId
|
|
AND addpersonnel4.rn = 2
|
|
AND addpersonnel4.EmpType = 'P'
|
|
WHERE
|
|
AFW_BasicPolInfo.PolExpDate >= @startDate
|
|
AND AFW_BasicPolInfo.PolExpDate <= @endDate
|
|
AND AFW_GeneralLedgerDepartment.ShortName IN ('Dept1', 'Dept8', 'Dept9', 'Dept10', 'Dept12')
|
|
AND AFW_BasicPolInfo.PolSubType = 'P'
|
|
AND AFW_BasicPolInfo.Status != 'D'
|
|
ORDER BY AFW_Customer.FirmNameCust
|
|
`
|
|
|
|
const result = await executeAfwQuery<AfwPolicy>(query, {
|
|
startDate: policyStartDate,
|
|
endDate: policyEndDate,
|
|
})
|
|
|
|
// Log first record to see what we're getting from AFW
|
|
if (result.length > 0) {
|
|
console.log('📋 Sample AFW Policy Data:', JSON.stringify(result[0], null, 2))
|
|
}
|
|
console.log(`📊 Total AFW policies fetched: ${result.length}`)
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Fetch employees from AFW database
|
|
*/
|
|
export async function fetchAfwEmployees(): Promise<AfwEmployee[]> {
|
|
const query = `
|
|
SELECT
|
|
EmpCode,
|
|
LastName,
|
|
FirstName,
|
|
MiddleName,
|
|
ShortName,
|
|
Title,
|
|
EMail,
|
|
Status,
|
|
IsRep,
|
|
IsProd,
|
|
EmpSupervisorCode,
|
|
DefaultGLDeptCode
|
|
FROM AFW_Employee
|
|
WHERE Status = 'A'
|
|
ORDER BY LastName, FirstName
|
|
`
|
|
|
|
return executeAfwQuery<AfwEmployee>(query)
|
|
}
|
|
|
|
/**
|
|
* Fetch GL departments from AFW database
|
|
*/
|
|
export async function fetchAfwGLDepartments(): Promise<AfwGLDepartment[]> {
|
|
const query = `
|
|
SELECT
|
|
GLDeptCode,
|
|
Name,
|
|
ShortName
|
|
FROM AFW_GeneralLedgerDepartment
|
|
WHERE Status = 'A'
|
|
ORDER BY Name
|
|
`
|
|
|
|
return executeAfwQuery<AfwGLDepartment>(query)
|
|
}
|
|
|
|
/**
|
|
* Fetch companies from AFW database
|
|
*/
|
|
export async function fetchAfwCompanies(): Promise<AfwCompany[]> {
|
|
const query = `
|
|
SELECT
|
|
CompanyCode,
|
|
CompanyName,
|
|
Address
|
|
FROM AFW_Company
|
|
ORDER BY CompanyName
|
|
`
|
|
|
|
return executeAfwQuery<AfwCompany>(query)
|
|
}
|
|
|
|
/**
|
|
* Fetch PR codes from AFW database
|
|
*/
|
|
export async function fetchAfwPRCodes(): Promise<AfwPRCode[]> {
|
|
const query = `
|
|
SELECT
|
|
PRCode,
|
|
PRName,
|
|
Description
|
|
FROM AFW_PRCode
|
|
ORDER BY PRName
|
|
`
|
|
|
|
return executeAfwQuery<AfwPRCode>(query)
|
|
}
|
|
|
|
/**
|
|
* Shape customer data structure (customers with specific ANotId)
|
|
*/
|
|
export interface AfwShapeCustomer {
|
|
CustId: string
|
|
CustNo: number
|
|
FirmNameCust: string
|
|
}
|
|
|
|
/**
|
|
* Fetch customers by ANotId from AFW database
|
|
* Generic function to fetch customers with any ANotId
|
|
*/
|
|
export async function fetchAfwCustomersByAnotId(anotId: string): Promise<AfwShapeCustomer[]> {
|
|
const query = `
|
|
SELECT
|
|
FirmNameCust,
|
|
CustNo,
|
|
CustId
|
|
FROM dbo.AFW_Customer
|
|
WHERE ANotId = @anotId
|
|
ORDER BY FirmNameCust
|
|
`
|
|
|
|
return executeAfwQuery<AfwShapeCustomer>(query, { anotId })
|
|
}
|
|
|
|
/**
|
|
* Fetch Shape program customers from AFW database
|
|
* These are customers identified by the specific ANotId for the SHAPE program
|
|
* @deprecated Use fetchAfwCustomersByAnotId instead for flexibility
|
|
*/
|
|
export async function fetchAfwShapeCustomers(): Promise<AfwShapeCustomer[]> {
|
|
return fetchAfwCustomersByAnotId('13CF7DCB-F6AF-42C2-A7AB-26641A216A81')
|
|
}
|
|
|
|
/**
|
|
* Fetch complete policy business view (similar to AFW_PolicyBizVw)
|
|
*/
|
|
export interface AfwPolicyBizView {
|
|
PolID: string
|
|
CustID: string
|
|
CustName: string
|
|
PolNum: string | null
|
|
PolType: string | null
|
|
ExpDate: Date
|
|
CarrierName: string | null
|
|
Premium: number | null
|
|
ProducerCode: string | null
|
|
GLDeptCode: string | null
|
|
GLDeptName: string | null
|
|
PrimaryEmpCode: string | null
|
|
PrimaryEmpName: string | null
|
|
}
|
|
|
|
export async function fetchAfwPolicyBizView(
|
|
expirationMonthsAhead: number = 24
|
|
): Promise<AfwPolicyBizView[]> {
|
|
const currentDate = new Date()
|
|
const futureDate = new Date()
|
|
futureDate.setMonth(futureDate.getMonth() + expirationMonthsAhead)
|
|
|
|
const query = `
|
|
SELECT
|
|
p.PolID,
|
|
p.CustID,
|
|
c.CustName,
|
|
p.PolNum,
|
|
p.PolType,
|
|
p.ExpDate,
|
|
p.CarrierName,
|
|
p.Premium,
|
|
c.ProducerCode,
|
|
gld.GLDeptCode,
|
|
gld.GLDeptName,
|
|
pp.EmpCode AS PrimaryEmpCode,
|
|
e.EmpName AS PrimaryEmpName
|
|
FROM AFW_BasicPolInfo p
|
|
INNER JOIN AFW_Customer c ON p.CustID = c.CustID
|
|
LEFT JOIN AFW_GeneralLedgerDepartment gld ON c.GLDeptCode = gld.GLDeptCode
|
|
LEFT JOIN AFW_PolicyPersonnel pp ON p.PolID = pp.PolID AND pp.IsPrimary = 1
|
|
LEFT JOIN AFW_Employee e ON pp.EmpCode = e.EmpCode
|
|
WHERE p.ExpDate >= @currentDate
|
|
AND p.ExpDate <= @futureDate
|
|
AND p.Status = 'Active'
|
|
ORDER BY p.ExpDate ASC
|
|
`
|
|
|
|
return executeAfwQuery<AfwPolicyBizView>(query, {
|
|
currentDate,
|
|
futureDate,
|
|
})
|
|
}
|