/* eslint-disable unicorn/filename-case, @eslint-community/eslint-comments/disable-enable-pair */ import type { Request, Response } from 'express' import ical, { type ICalEventData, ICalEventStatus } from 'ical-generator' import { type WorkOrderMilestoneFilters, getWorkOrderMilestones } from '../../database/getWorkOrderMilestones.js' import * as configFunctions from '../../helpers/functions.config.js' import { getPrintConfig } from '../../helpers/functions.print.js' import type { WorkOrderMilestone } from '../../types/recordTypes.js' const calendarCompany = 'cityssm.github.io' const calendarProduct = configFunctions.getConfigProperty( 'application.applicationName' ) const timeStringSplitRegex = /[ :-]/ function escapeHTML(stringToEscape: string): string { return stringToEscape.replaceAll( /[^\d a-z]/gi, (c) => `&#${c.codePointAt(0)!};` ) } function getUrlRoot(request: Request): string { return ( 'http://' + request.hostname + (configFunctions.getConfigProperty('application.httpPort') === 80 ? '' : `:${configFunctions.getConfigProperty('application.httpPort')}`) + configFunctions.getConfigProperty('reverseProxy.urlPrefix') ) } function getWorkOrderUrl( request: Request, milestone: WorkOrderMilestone ): string { return `${getUrlRoot(request)}/workOrders/${milestone.workOrderId!}` } function buildEventSummary(milestone: WorkOrderMilestone): string { let summary = (milestone.workOrderMilestoneCompletionDate ? '✔ ' : '') + ((milestone.workOrderMilestoneTypeId ?? -1) === -1 ? milestone.workOrderMilestoneDescription ?? '' : milestone.workOrderMilestoneType ?? '' ).trim() let occupantCount = 0 for (const lotOccupancy of milestone.workOrderLotOccupancies!) { for (const occupant of lotOccupancy.lotOccupancyOccupants!) { occupantCount += 1 if (occupantCount === 1) { if (summary !== '') { summary += ': ' } summary += (occupant.occupantName ?? '') + ' ' + (occupant.occupantFamilyName ?? '') } } } if (occupantCount > 1) { summary += ` plus ${(occupantCount - 1).toString()}` } return summary } // eslint-disable-next-line @typescript-eslint/naming-convention function buildEventDescriptionHTML_occupancies( request: Request, milestone: WorkOrderMilestone ): string { let descriptionHTML = '' if (milestone.workOrderLotOccupancies!.length > 0) { const urlRoot = getUrlRoot(request) descriptionHTML = `

Related ${escapeHTML(configFunctions.getConfigProperty('aliases.occupancies'))}

` for (const occupancy of milestone.workOrderLotOccupancies!) { descriptionHTML += `' } descriptionHTML += '
${escapeHTML( configFunctions.getConfigProperty('aliases.occupancy') )} Type ${escapeHTML(configFunctions.getConfigProperty('aliases.lot'))} Start Date End Date ${escapeHTML(configFunctions.getConfigProperty('aliases.occupants'))}
${escapeHTML(occupancy.occupancyType!)} ${occupancy.lotName ? escapeHTML(occupancy.lotName) : '(Not Set)'} ${occupancy.occupancyStartDateString!} ${ occupancy.occupancyEndDate ? occupancy.occupancyEndDateString! : '(No End Date)' } ` for (const occupant of occupancy.lotOccupancyOccupants!) { descriptionHTML += `${escapeHTML( occupant.lotOccupantType! )}: ${escapeHTML(occupant.occupantName!)} ${escapeHTML( occupant.occupantFamilyName! )}
` } descriptionHTML += '
' } return descriptionHTML } // eslint-disable-next-line @typescript-eslint/naming-convention function buildEventDescriptionHTML_lots( request: Request, milestone: WorkOrderMilestone ): string { let descriptionHTML = '' if (milestone.workOrderLots!.length > 0) { const urlRoot = getUrlRoot(request) descriptionHTML += `

Related ${escapeHTML(configFunctions.getConfigProperty('aliases.lots'))}

` for (const lot of milestone.workOrderLots!) { descriptionHTML += `` } descriptionHTML += '
${escapeHTML(configFunctions.getConfigProperty('aliases.lot'))} Type ${escapeHTML(configFunctions.getConfigProperty('aliases.map'))} ${escapeHTML(configFunctions.getConfigProperty('aliases.lot'))} Type Status
${escapeHTML(lot.lotName ?? '')} ${escapeHTML(lot.mapName ?? '')} ${escapeHTML(lot.lotType ?? '')} ${escapeHTML(lot.lotStatus ?? '')}
' } return descriptionHTML } // eslint-disable-next-line @typescript-eslint/naming-convention function buildEventDescriptionHTML_prints( request: Request, milestone: WorkOrderMilestone ): string { let descriptionHTML = '' const prints = configFunctions.getConfigProperty('settings.workOrders.prints') if (prints.length > 0) { const urlRoot = getUrlRoot(request) descriptionHTML += '

Prints

' for (const printName of prints) { const printConfig = getPrintConfig(printName) if (printConfig) { descriptionHTML += `

${escapeHTML(printConfig.title)}
${urlRoot}/print/${printName}/?workOrderId=${milestone.workOrderId!.toString()}

` } } } return descriptionHTML } function buildEventDescriptionHTML( request: Request, milestone: WorkOrderMilestone ): string { const workOrderUrl = getWorkOrderUrl(request, milestone) let descriptionHTML = `

Milestone Description

${escapeHTML(milestone.workOrderMilestoneDescription ?? '')}

Work Order #${milestone.workOrderNumber ?? ''}

${escapeHTML(milestone.workOrderDescription ?? '')}

${workOrderUrl}

` descriptionHTML += buildEventDescriptionHTML_occupancies(request, milestone) descriptionHTML += buildEventDescriptionHTML_lots(request, milestone) descriptionHTML += buildEventDescriptionHTML_prints(request, milestone) return descriptionHTML } function buildEventCategoryList(milestone: WorkOrderMilestone): string[] { const categories: string[] = [] if (milestone.workOrderMilestoneTypeId) { categories.push( milestone.workOrderMilestoneType!, milestone.workOrderType ?? '' ) } if (milestone.workOrderMilestoneCompletionDate) { categories.push('Completed') } return categories } function buildEventLocation(milestone: WorkOrderMilestone): string { const lotNames: string[] = [] if (milestone.workOrderLots!.length > 0) { for (const lot of milestone.workOrderLots!) { lotNames.push(`${lot.mapName ?? ''}: ${lot.lotName ?? ''}`) } } return lotNames.join(', ') } export async function handler( request: Request, response: Response ): Promise { const urlRoot = getUrlRoot(request) /* * Get work order milestones */ const workOrderMilestoneFilters: WorkOrderMilestoneFilters = { workOrderTypeIds: request.query.workOrderTypeIds as string, workOrderMilestoneTypeIds: request.query.workOrderMilestoneTypeIds as string } if (request.query.workOrderId) { workOrderMilestoneFilters.workOrderId = request.query.workOrderId as string workOrderMilestoneFilters.workOrderMilestoneDateFilter = 'notBlank' } else { workOrderMilestoneFilters.workOrderMilestoneDateFilter = 'recent' } const workOrderMilestones = await getWorkOrderMilestones( workOrderMilestoneFilters, { includeWorkOrders: true, orderBy: 'date' } ) /* * Create calendar object */ const calendar = ical({ name: 'Work Order Milestone Calendar', url: `${urlRoot}/workOrders` }) if (request.query.workOrderId && workOrderMilestones.length > 0) { calendar.name(`Work Order #${workOrderMilestones[0].workOrderNumber}`) calendar.url( `${urlRoot}/workOrders/${workOrderMilestones[0].workOrderId!.toString()}` ) } calendar.prodId({ company: calendarCompany, product: calendarProduct }) /* * Loop through milestones */ for (const milestone of workOrderMilestones) { const milestoneTimePieces = `${milestone.workOrderMilestoneDateString} ${milestone.workOrderMilestoneTimeString}`.split( timeStringSplitRegex ) const milestoneDate = new Date( Number.parseInt(milestoneTimePieces[0], 10), Number.parseInt(milestoneTimePieces[1], 10) - 1, Number.parseInt(milestoneTimePieces[2], 10), Number.parseInt(milestoneTimePieces[3], 10), Number.parseInt(milestoneTimePieces[4], 10) ) const milestoneEndDate = new Date(milestoneDate.getTime()) milestoneEndDate.setHours(milestoneEndDate.getHours() + 1) // Build summary (title in Outlook) const summary = buildEventSummary(milestone) // Build URL const workOrderUrl = getWorkOrderUrl(request, milestone) // Create event const eventData: ICalEventData = { start: milestoneDate, created: new Date(milestone.recordCreate_timeMillis!), stamp: new Date(milestone.recordCreate_timeMillis!), lastModified: new Date( Math.max( milestone.recordUpdate_timeMillis!, milestone.workOrderRecordUpdate_timeMillis! ) ), allDay: !milestone.workOrderMilestoneTime, summary, url: workOrderUrl } if (!eventData.allDay) { eventData.end = milestoneEndDate } const calendarEvent = calendar.createEvent(eventData) // Build description const descriptionHTML = buildEventDescriptionHTML(request, milestone) calendarEvent.description({ plain: workOrderUrl, html: descriptionHTML }) // Set status if (milestone.workOrderMilestoneCompletionDate) { calendarEvent.status(ICalEventStatus.CONFIRMED) } // Add categories const categories = buildEventCategoryList(milestone) for (const category of categories) { calendarEvent.createCategory({ name: category }) } // Set location const location = buildEventLocation(milestone) calendarEvent.location(location) // Set organizer / attendees if (milestone.workOrderLotOccupancies!.length > 0) { let organizerSet = false for (const lotOccupancy of milestone.workOrderLotOccupancies!) { for (const occupant of lotOccupancy.lotOccupancyOccupants!) { if (organizerSet) { calendarEvent.createAttendee({ name: `${occupant.occupantName ?? ''} ${ occupant.occupantFamilyName ?? '' }`, email: configFunctions.getConfigProperty( 'settings.workOrders.calendarEmailAddress' ) }) } else { calendarEvent.organizer({ name: `${occupant.occupantName ?? ''} ${ occupant.occupantFamilyName ?? '' }`, email: configFunctions.getConfigProperty( 'settings.workOrders.calendarEmailAddress' ) }) organizerSet = true } } } } else { calendarEvent.organizer({ name: milestone.recordCreate_userName!, email: configFunctions.getConfigProperty( 'settings.workOrders.calendarEmailAddress' ) }) } } calendar.serve(response) } export default handler