Last active
October 20, 2023 03:14
-
-
Save kukiron/24e3f94f78229a4c4e38713f45bdc200 to your computer and use it in GitHub Desktop.
Vessel centric view utils
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import differenceBy from 'lodash/differenceBy'; | |
import flatten from 'lodash/flatten'; | |
import partition from 'lodash/partition'; | |
import moment from 'moment'; | |
import { getTimelineMonths } from '../helpers'; | |
import { | |
CrewSchedule, | |
FormattedSchedules, | |
RowSegment, | |
TimelineMonth, | |
VesselCrewScheduleResponse, | |
} from '../types'; | |
// module containing utils for vessel-centric view | |
export class CrewScheduleModule { | |
scheduleResponse: VesselCrewScheduleResponse; | |
constructor(scheduleResponse: VesselCrewScheduleResponse) { | |
this.scheduleResponse = scheduleResponse; | |
} | |
get timelineMonths(): TimelineMonth[] { | |
const sortedCrewSchedules = flatten( | |
Object.values(this.scheduleResponse) | |
).sort((a, b) => a.signOnDate.localeCompare(b.signOnDate)); | |
// earliest crew `signOnDate` in the screw schedule response | |
const earliest = sortedCrewSchedules[0].signOnDate; | |
// LATEST crew `signOnDate` in the screw schedule response | |
const latest = | |
sortedCrewSchedules[sortedCrewSchedules.length - 1].signOnDate; | |
return getTimelineMonths(earliest, latest); | |
} | |
// convert ISO date string to `YYYY-MM-DD` format | |
private trimDate(dateStr: string) { | |
return dateStr.split('T')[0]; | |
} | |
private findDatesDifference(date1: string, date2: string) { | |
return moment(date1).diff(moment(date2), 'days'); | |
} | |
// check if a scpecific schedule is historical | |
private isHistorical(schedule: CrewSchedule) { | |
const { signOnDateType, signOffDateType } = schedule; | |
return signOnDateType === 'RECORD' && signOffDateType === 'RECORD'; | |
} | |
// checks if 2 crew schedules are within 2 days | |
private areEventsWithin2Days(event1: CrewSchedule, event2: CrewSchedule) { | |
const difference1 = this.findDatesDifference( | |
event2.signOnDate, | |
event1.signOffDate | |
); | |
const difference2 = this.findDatesDifference( | |
event1.signOnDate, | |
event2.signOffDate | |
); | |
return Math.abs(difference1) <= 2 || Math.abs(difference2) <= 2; | |
} | |
private getCustomKey(details: CrewSchedule, past: boolean) { | |
return past | |
? `${this.trimDate(details.signOffDate)}--${details.externalCrewId}` | |
: `${this.trimDate(details.signOnDate)}--${details.externalCrewId}`; | |
} | |
private removeId(schedules: (CrewSchedule & { id?: number })[]) { | |
return schedules.map(({ id, ...rest }) => ({ ...rest })); | |
} | |
// takes schedule items array & convert to key-value pair | |
// values have grouped schedules that match sign-off & sign-on date | |
private formatToScheduleGroups( | |
schedules: CrewSchedule[], | |
past: boolean = false | |
) { | |
if (schedules.length === 1) { | |
return { [this.getCustomKey(schedules[0], past)]: schedules }; | |
} | |
const formattedSchedules = schedules.map((item, index) => ({ | |
...item, | |
id: index, | |
})); | |
return formattedSchedules.reduce<{ | |
[date: string]: CrewSchedule[]; | |
}>((acc, item) => { | |
const includedSchedules = flatten(Object.values(acc)); | |
const remainingSchedules = differenceBy( | |
formattedSchedules, | |
includedSchedules, | |
'id' | |
); | |
if (!remainingSchedules.length) { | |
return acc; | |
} | |
const replacementCrew = remainingSchedules.find( | |
(crew) => | |
crew.externalCrewId === item.signOffEvent?.replacementCrewId || | |
this.areEventsWithin2Days(item, crew) | |
); | |
if (replacementCrew) { | |
const pairedCrew = [item, replacementCrew].sort((a, b) => | |
a.signOnDate.localeCompare(b.signOnDate) | |
); | |
const customKey = this.getCustomKey( | |
past ? pairedCrew[pairedCrew.length - 1] : pairedCrew[0], | |
past | |
); | |
return { ...acc, [customKey]: pairedCrew }; | |
} | |
return { ...acc, [this.getCustomKey(item, past)]: [item] }; | |
}, {}); | |
} | |
// finds schedule obj key that has matching date with input date str | |
// used to find matching past schedules for a group of future schedules | |
private findMatchingDate(stringArr: string[], dateStr: string) { | |
const [pastDate, pastCrewId] = dateStr.split('--'); | |
return stringArr.reduce<string>((prev, curr) => { | |
const [currDate, crewId] = curr.split('--'); | |
const [prevDate] = prev.split('--'); | |
if (pastCrewId === crewId) return prev; | |
const diffWithCurrDate = this.findDatesDifference(currDate, pastDate); | |
if (Math.abs(diffWithCurrDate) <= 7) return curr; | |
const diffWithPrevDate = this.findDatesDifference(prevDate, pastDate); | |
return diffWithCurrDate <= diffWithPrevDate ? curr : prev; | |
}, ''); | |
} | |
// remove a group of shcedules that include any historical item inside | |
// this is to provide a trimmed schedule groups in each iteration | |
// to insert the accurate past schedules to a group of future schedules | |
private removeHistoricalScheduleGroup(scheduleGroup: { | |
[key: string]: CrewSchedule[]; | |
}) { | |
return Object.keys(scheduleGroup).reduce<{ | |
[key: string]: CrewSchedule[]; | |
}>((acc, key) => { | |
const schedules = scheduleGroup[key]; | |
const hasHistorical = schedules.some(this.isHistorical); | |
return hasHistorical ? acc : { ...acc, [key]: schedules }; | |
}, {}); | |
} | |
// formats all the schedules for a specific rank | |
// generates groups of schedules arrays which are then represented in a timeline array | |
private formatRankSchedules(rankSchedules: CrewSchedule[], rank?: string) { | |
const [pastSchedules, futureSchedules] = partition( | |
rankSchedules, | |
this.isHistorical | |
); | |
const futureScheduleGroups = this.formatToScheduleGroups(futureSchedules); | |
if (!pastSchedules.length) { | |
return Object.values(futureScheduleGroups); | |
} | |
const pastScheduleGroups = this.formatToScheduleGroups(pastSchedules, true); | |
if (!futureSchedules.length) { | |
return Object.values(pastScheduleGroups); | |
} | |
const result = Object.keys(pastScheduleGroups).reduce<{ | |
[date: string]: CrewSchedule[]; | |
}>((acc, dateStr) => { | |
const matchedKey = this.findMatchingDate( | |
Object.keys(this.removeHistoricalScheduleGroup(acc)), | |
dateStr | |
); | |
const updatedItem = [ | |
...(pastScheduleGroups[dateStr] || []), | |
...(futureScheduleGroups[matchedKey] || []), | |
]; | |
return { | |
...acc, | |
...(updatedItem.length ? { [matchedKey]: updatedItem } : {}), | |
}; | |
}, futureScheduleGroups); | |
return Object.values(result) | |
.map(this.removeId) // remove inserted `id` property (in previous step) | |
.sort((a, b) => a[0].signOnDate.localeCompare(b[0].signOnDate)); | |
} | |
public getFormattedCrewSchedules(): FormattedSchedules { | |
return Object.keys(this.scheduleResponse).reduce( | |
(acc, rank) => ({ | |
...acc, | |
[rank]: this.formatRankSchedules(this.scheduleResponse[rank], rank), | |
}), | |
{} | |
); | |
} | |
// generates segments/sections of a timeline row with details | |
// all of which total to 100% | |
public getTimelineRowDetails(rowSchedules: CrewSchedule[]): RowSegment[] { | |
// starting point in the timeline row | |
const { date: startDate } = this.timelineMonths[0]; | |
// start date of ending month | |
const { date: endMonthStartDate } = | |
this.timelineMonths[this.timelineMonths.length - 1]; | |
// finishing point of timeline roe | |
const endDate = moment(endMonthStartDate).add(1, 'month'); | |
// total number of days in the timeline row | |
const totalDays = moment(endDate).diff(moment(startDate), 'days'); | |
// sign-on date of first crew in the order | |
const firstScheduleDate = rowSchedules[0].signOnDate; | |
// sign-off date of last crew in the order - could be beyond the timeline `endDate` | |
// if the `signOffDateType` is `PLACEHOLDER` | |
const lastScheduleDate = rowSchedules[rowSchedules.length - 1].signOffDate; | |
// first segment of the timeline row | |
// section info available if the `startDate` is before `firstScheduleDate` (most cases) | |
// otherwise, empty array | |
let initialSection: RowSegment[] = []; | |
// last segment of the timeline row | |
// section info available if the `endDate` is after `lastScheduleDate` | |
// otherwise, empty array - in which case `lastScheduleDate` goes beynd timeline `endDate` | |
let finalSection: RowSegment[] = []; | |
// find difference between timeline `startDate` & `firstScheduleDate` | |
const initialDifference = moment(firstScheduleDate).diff( | |
moment(startDate), | |
'days' | |
); | |
// if there's a positive difference, `initialSection` is NOT an empty array | |
if (initialDifference > 0) { | |
initialSection = [ | |
{ | |
width: Math.ceil((initialDifference / totalDays) * 100), | |
empty: true, | |
}, | |
]; | |
} | |
// find difference between timeline `endDate` & `lastScheduleDate` | |
const finalDifference = moment(endDate).diff( | |
moment(lastScheduleDate), | |
'days' | |
); | |
// if there's a positive difference, `finalSection` is NOT an empty array | |
if (finalDifference > 0) { | |
finalSection = [ | |
{ width: (finalDifference / totalDays) * 100, empty: true }, | |
]; | |
} | |
// now generate the sections/segments for all crew schedules for a timeline row | |
const crewSegments = rowSchedules.reduce<RowSegment[]>( | |
(acc, item, index) => { | |
const { name, signOnDate, signOffDate, signOffDateType } = item; | |
let difference = 0; | |
if (index + 1 < rowSchedules.length) { | |
const nextItem = rowSchedules[index + 1]; | |
difference = moment(nextItem.signOnDate).diff( | |
moment(signOnDate), | |
'days' | |
); | |
} else { | |
difference = moment( | |
finalDifference > 0 ? item.signOffDate : endDate | |
).diff(moment(signOnDate), 'days'); | |
} | |
return [ | |
...acc, | |
{ | |
width: (difference / totalDays) * 100, | |
past: this.isHistorical(item), | |
name: name, | |
signOnDate, | |
...(signOffDateType !== 'PLACEHOLDER' ? { signOffDate } : {}), | |
}, | |
]; | |
}, | |
[] | |
); | |
// padded with empty segments at the start & end | |
return [...initialSection, ...crewSegments, ...finalSection]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment