/* eslint-disable unicorn/filename-case */ import ical, { type ICalEventData, ICalEventStatus } from 'ical-generator' import { getWorkOrderMilestones, type WorkOrderMilestoneFilters } from '../../helpers/lotOccupancyDB/getWorkOrderMilestones.js' import type { Request, Response } from 'express' import * as configFunctions from '../../helpers/functions.config.js' import { getPrintConfig } from '../../helpers/functions.print.js' import type * as recordTypes from '../../types/recordTypes' const calendarCompany = 'cityssm.github.io' const calendarProduct = configFunctions.getProperty( 'application.applicationName' ) const timeStringSplitRegex = /[ :-]/ function escapeHTML(stringToEscape: string): string { return stringToEscape.replace( /[^\d A-Za-z]/g, (c) => `&#${c.codePointAt(0)!};` ) } function getUrlRoot(request: Request): string { return ( 'http://' + request.hostname + (configFunctions.getProperty('application.httpPort') === 80 ? '' : `:${configFunctions.getProperty('application.httpPort')}`) + configFunctions.getProperty('reverseProxy.urlPrefix') ) } function getWorkOrderUrl( request: Request, milestone: recordTypes.WorkOrderMilestone ): string { return `${getUrlRoot(request)}/workOrders/${milestone.workOrderId!}` } function buildEventSummary(milestone: recordTypes.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: recordTypes.WorkOrderMilestone ): string { let descriptionHTML = '' if (milestone.workOrderLotOccupancies!.length > 0) { const urlRoot = getUrlRoot(request) descriptionHTML = `

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

` for (const occupancy of milestone.workOrderLotOccupancies!) { descriptionHTML += `' } descriptionHTML += '
${escapeHTML( configFunctions.getProperty('aliases.occupancy') )} Type ${escapeHTML(configFunctions.getProperty('aliases.lot'))} Start Date End Date ${escapeHTML(configFunctions.getProperty('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: recordTypes.WorkOrderMilestone ): string { let descriptionHTML = '' if (milestone.workOrderLots!.length > 0) { const urlRoot = getUrlRoot(request) descriptionHTML += '

Related ' + escapeHTML(configFunctions.getProperty('aliases.lots')) + '

' + '' + `` + '' + '' + '' for (const lot of milestone.workOrderLots!) { descriptionHTML += '' + ('') + `` + `` + `` + '' } descriptionHTML += '
${escapeHTML(configFunctions.getProperty('aliases.lot'))} Type ${escapeHTML(configFunctions.getProperty('aliases.map'))} ${escapeHTML(configFunctions.getProperty('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: recordTypes.WorkOrderMilestone ): string { let descriptionHTML = '' const prints = configFunctions.getProperty('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) + '

' } } } return descriptionHTML } function buildEventDescriptionHTML( request: Request, milestone: recordTypes.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: recordTypes.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: recordTypes.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 } 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) } 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.getProperty( 'settings.workOrders.calendarEmailAddress' ) }) } else { calendarEvent.organizer({ name: occupant.occupantName + ' ' + occupant.occupantFamilyName, email: configFunctions.getProperty( 'settings.workOrders.calendarEmailAddress' ) }) organizerSet = true } } } } else { calendarEvent.organizer({ name: milestone.recordCreate_userName, email: configFunctions.getProperty( 'settings.workOrders.calendarEmailAddress' ) }) } } calendar.serve(response) } export default handler