From fffb59b76e89d13e91ed5b49b3c6c517dbdede20 Mon Sep 17 00:00:00 2001
From: Ramesh Kumar <ramesh.p@matriotsolutions.com>
Date: Wed, 24 Mar 2021 13:16:45 +0530
Subject: [PATCH] TMSS-674: Added Reserrvation block in week timeline view.
 Also added mouseover popup dialog for Reservation blocks.

---
 .../components/Timeline/CalendarTimeline.js   |   6 +-
 .../src/routes/Timeline/week.view.js          | 259 ++++++++++++++++--
 2 files changed, 242 insertions(+), 23 deletions(-)

diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js
index 19ee68919a9..a88c818fcdb 100644
--- a/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js
+++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Timeline/CalendarTimeline.js
@@ -814,7 +814,7 @@ export class CalendarTimeline extends Component {
      * @param {Object} item 
      */
     onItemMouseOver(evt, item) {
-        if ((item.type==="SCHEDULE" || item.type==="TASK") && this.props.itemMouseOverCallback) {
+        if ((item.type==="SCHEDULE" || item.type==="TASK" || item.type==="RESERVATION") && this.props.itemMouseOverCallback) {
             this.props.itemMouseOverCallback(evt, item);
         }
     }
@@ -824,7 +824,7 @@ export class CalendarTimeline extends Component {
      * @param {Object} item 
      */
     onItemMouseOut(evt, item) {
-        if ((item.type==="SCHEDULE" || item.type==="TASK") && this.props.itemMouseOutCallback) {
+        if ((item.type==="SCHEDULE" || item.type==="TASK" || item.type==="RESERVATION") && this.props.itemMouseOutCallback) {
             this.props.itemMouseOutCallback(evt);
         }
     }
@@ -1345,7 +1345,7 @@ export class CalendarTimeline extends Component {
                             <div className='col-1 su-legend su-finished' title="Finished">Finished</div>
                         </div>
                     </div>
-                    {!this.props.showSunTimings && this.state.viewType===UIConstants.timeline.types.NORMAL &&
+                    {!this.props.showSunTimings && 
                     <div className="col-3">
                         <div style={{fontWeight:'500', height: '25px'}}>Station Reservation</div>
                         <div className="p-grid">
diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js
index fa976d92bec..82e62ab263b 100644
--- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js
+++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js
@@ -23,6 +23,8 @@ import SchedulingUnitSummary from '../Scheduling/summary';
 import UIConstants from '../../utils/ui.constants';
 import { OverlayPanel } from 'primereact/overlaypanel';
 import { TieredMenu } from 'primereact/tieredmenu';
+import { InputSwitch } from 'primereact/inputswitch';
+import { Dropdown } from 'primereact/dropdown';
 
 // Color constant for status
 const STATUS_COLORS = { "ERROR": "FF0000", "CANCELLED": "#00FF00", "DEFINED": "#00BCD4", 
@@ -30,6 +32,9 @@ const STATUS_COLORS = { "ERROR": "FF0000", "CANCELLED": "#00FF00", "DEFINED": "#
                         "OBSERVED": "#cde", "PROCESSING": "#cddc39", "PROCESSED": "#fed",
                         "INGESTING": "#edc", "FINISHED": "#47d53d"};
 
+const RESERVATION_COLORS = {"true-true":{bgColor:"lightgrey", color:"#585859"}, "true-false":{bgColor:'#585859', color:"white"},
+                            "false-true":{bgColor:"#9b9999", color:"white"}, "false-false":{bgColor:"black", color:"white"}};
+
 /**
  * Scheduling Unit timeline view component to view SU List and timeline
  */
@@ -50,10 +55,13 @@ export class WeekTimelineView extends Component {
             selectedItem: null,
             suTaskList:[],
             isSummaryLoading: false,
-            stationGroup: []
+            stationGroup: [],
+            reservationEnabled: true
         }
         this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable'];  // Statuses before scheduled to get station_group
         this.mainStationGroups = {};
+        this.reservations = [];
+        this.reservationReasons = [];
         this.optionsMenu = React.createRef();
         this.menuOptions = [ {label:'Add Reservation', icon: "fa fa-", command: () => {this.selectOptionMenu('Add Reservation')}}, 
                             {label:'Reservation List', icon: "fa fa-", command: () => {this.selectOptionMenu('Reservation List')}},
@@ -68,6 +76,7 @@ export class WeekTimelineView extends Component {
         this.dateRangeCallback = this.dateRangeCallback.bind(this);
         this.resizeSUList = this.resizeSUList.bind(this);
         this.suListFilterCallback = this.suListFilterCallback.bind(this);
+        this.addWeekReservations = this.addWeekReservations.bind(this);
         this.handleData = this.handleData.bind(this);
         this.addNewData = this.addNewData.bind(this);
         this.updateExistingData = this.updateExistingData.bind(this);
@@ -75,21 +84,33 @@ export class WeekTimelineView extends Component {
     }
 
     async componentDidMount() {
+        UtilService.getReservationTemplates().then(templates => {
+            this.reservationTemplate = templates.length>0?templates[0]:null;
+            if (this.reservationTemplate) {
+                let reasons = this.reservationTemplate.schema.properties.activity.properties.type.enum;
+                for (const reason of reasons) {
+                    this.reservationReasons.push({name: reason});
+                }
+            }
+        });
+        
         // Fetch all details from server and prepare data to pass to timeline and table components
         const promises = [  ProjectService.getProjectList(), 
                             ScheduleService.getSchedulingUnitsExtended('blueprint'),
                             ScheduleService.getSchedulingUnitDraft(),
                             ScheduleService.getSchedulingSets(),
                             UtilService.getUTC(),
-                            TaskService.getSubtaskTemplates()] ;
+                            TaskService.getSubtaskTemplates(),
+                            UtilService.getReservations()] ;
         Promise.all(promises).then(async(responses) => {
             this.subtaskTemplates = responses[5];
             const projects = responses[0];
             const suBlueprints = _.sortBy(responses[1], 'name');
             const suDrafts = responses[2].data.results;
             const suSets = responses[3]
-            const group = [], items = [];
+            let group = [], items = [];
             const currentUTC = moment.utc(responses[4]);
+            this.reservations = responses[6];
             const defaultStartTime = moment.utc().day(-2).hour(0).minutes(0).seconds(0);
             const defaultEndTime = moment.utc().day(8).hour(23).minutes(59).seconds(59);
             for (const count of _.range(11)) {
@@ -150,6 +171,9 @@ export class WeekTimelineView extends Component {
                     }
                 }
             }
+            if (this.state.reservationEnabled) {
+                items = this.addWeekReservations(items, defaultStartTime, defaultEndTime, currentUTC);
+            }
             // Get all scheduling constraint templates
             ScheduleService.getSchedulingConstraintTemplates()
                 .then(suConstraintTemplates => {
@@ -263,23 +287,36 @@ export class WeekTimelineView extends Component {
      * @param {Object} item
      */
     onItemMouseOver(evt, item) {
-        const itemSU = _.find(this.state.suBlueprints, {id: parseInt(item.id.split("-")[0])});
-        const itemStations = itemSU.stations;
-        const itemStationGroups = this.groupSUStations(itemStations);
-        item.stations = {groups: "", counts: ""};
-        for (const stationgroup of _.keys(itemStationGroups)) {
-            let groups = item.stations.groups;
-            let counts = item.stations.counts;
-            if (groups) {
-                groups = groups.concat("/");
-                counts = counts.concat("/");
+        if (item.type === "SCHEDULE") {
+            const itemSU = _.find(this.state.suBlueprints, {id: parseInt(item.id.split("-")[0])});
+            const itemStations = itemSU.stations;
+            const itemStationGroups = this.groupSUStations(itemStations);
+            item.stations = {groups: "", counts: ""};
+            for (const stationgroup of _.keys(itemStationGroups)) {
+                let groups = item.stations.groups;
+                let counts = item.stations.counts;
+                if (groups) {
+                    groups = groups.concat("/");
+                    counts = counts.concat("/");
+                }
+                groups = groups.concat(stationgroup.substring(0,1).concat('S'));
+                counts = counts.concat(itemStationGroups[stationgroup].length);
+                item.stations.groups = groups;
+                item.stations.counts = counts;
+                item.suStartTime = moment.utc(itemSU.start_time);
+                item.suStopTime = moment.utc(itemSU.stop_time);
             }
-            groups = groups.concat(stationgroup.substring(0,1).concat('S'));
-            counts = counts.concat(itemStationGroups[stationgroup].length);
-            item.stations.groups = groups;
-            item.stations.counts = counts;
-            item.suStartTime = moment.utc(itemSU.start_time);
-            item.suStopTime = moment.utc(itemSU.stop_time);
+        }   else {
+            const reservation = _.find(this.reservations, {'id': parseInt(item.id.split("-")[1])});
+            const reservStations = reservation.specifications_doc.resources.stations;
+            const reservStationGroups = this.groupSUStations(reservStations);
+            item.name = reservation.name;
+            item.contact = reservation.specifications_doc.activity.contact
+            item.activity_type = reservation.specifications_doc.activity.type;
+            item.stations = reservStations;
+            item.planned = reservation.specifications_doc.activity.planned;
+            item.displayStartTime = moment.utc(reservation.start_time);
+            item.displayEndTime = reservation.duration?moment.utc(reservation.stop_time):null;
         }
         this.popOver.toggle(evt);
         this.setState({mouseOverItem: item});
@@ -362,6 +399,9 @@ export class WeekTimelineView extends Component {
                         }
                     } 
                 }
+                if (this.state.reservationEnabled) {
+                    items = this.addWeekReservations(items, startTime, endTime, currentUTC);
+                }
             }   else {
                 suBlueprintList = _.clone(this.state.suBlueprints);
                 group = this.state.group;
@@ -578,6 +618,132 @@ export class WeekTimelineView extends Component {
         });
     }
 
+    async showReservations(e) {
+        await this.setState({reservationEnabled: e.value});
+        let updatedItemGroupData = await this.dateRangeCallback(this.state.startTime, this.state.endTime, true);
+        this.timeline.updateTimeline(updatedItemGroupData);
+    }
+
+    /**
+     * Add Week Reservations during the visible timeline period
+     * @param {Array} items 
+     * @param {moment} startTime
+     * @param {moment} endTime
+     */
+     addWeekReservations(items, startTime, endTime, currentUTC) {
+        let reservations = this.reservations;
+        for (const reservation of reservations) {
+            const reservationStartTime = moment.utc(reservation.start_time);
+            const reservationEndTime = reservation.duration?reservationStartTime.clone().add(reservation.duration, 'seconds'):endTime;
+            const reservationSpec = reservation.specifications_doc;
+            if ( (reservationStartTime.isSame(startTime) 
+                    || reservationStartTime.isSame(endTime)                       
+                    || reservationStartTime.isBetween(startTime, endTime)
+                    || reservationEndTime.isSame(startTime) 
+                    || reservationEndTime.isSame(endTime)                       
+                    || reservationEndTime.isBetween(startTime, endTime)
+                    || (reservationStartTime.isSameOrBefore(startTime)
+                    && reservationEndTime.isSameOrAfter(endTime)))
+                    && (!this.state.reservationFilter ||                                        // No reservation filter added
+                        reservationSpec.activity.type === this.state.reservationFilter) ) {     // Reservation reason == Filtered reaseon
+                reservation.stop_time = reservationEndTime;
+                let splitReservations = this.splitReservations(reservation, startTime, endTime, currentUTC);
+                for (const splitReservation of splitReservations) {
+                    items.push(this.getReservationItem(splitReservation, currentUTC));
+                }
+                
+            }
+        }
+        return items;
+    }
+
+    /**
+     * Function to check if a reservation is for more than a day and split it to multiple objects to display in each day
+     * @param {Object} reservation - Reservation object
+     * @param {moment} startTime - moment object of the start datetime of the week view
+     * @param {moment} endTime  - moment object of the end datetime of the week view
+     * @returns 
+     */
+    splitReservations(reservation, startTime, endTime) {
+        const reservationStartTime = moment.utc(reservation.start_time);
+        let weekStartDate = moment(startTime).add(-1, 'day').startOf('day');
+        let weekEndDate = moment(endTime).add(1, 'day').startOf('day');
+        let splitReservations = [];
+        while(weekStartDate.add(1, 'days').diff(weekEndDate) < 0) {
+            const dayStart = weekStartDate.clone().startOf('day');
+            const dayEnd = weekStartDate.clone().endOf('day');
+            let splitReservation = null;
+            if (reservationStartTime.isSameOrBefore(dayStart) && 
+                (reservation.stop_time.isBetween(dayStart, dayEnd) ||
+                    reservation.stop_time.isSameOrAfter(dayEnd))) {
+                splitReservation = _.cloneDeep(reservation);
+                splitReservation.start_time = moment.utc(dayStart.format("YYYY-MM-DD HH:mm:ss"));
+            }   else if(reservationStartTime.isBetween(dayStart, dayEnd)) {
+                splitReservation = _.cloneDeep(reservation);
+                splitReservation.start_time = reservationStartTime;                
+            }
+            if (splitReservation) {
+                if (!reservation.stop_time || reservation.stop_time.isSameOrAfter(dayEnd)) {
+                    splitReservation.end_time = weekStartDate.clone().hour(23).minute(59).seconds(59);
+                }   else if (reservation.stop_time.isSameOrBefore(dayEnd)) {
+                    splitReservation.end_time = weekStartDate.clone().hour(reservation.stop_time.hours()).minutes(reservation.stop_time.minutes()).seconds(reservation.stop_time.seconds);
+                }
+                splitReservations.push(splitReservation);
+            }
+        }
+        return splitReservations;
+    }
+
+    /**
+     * Get reservation timeline item. If the reservation doesn't have duration, item endtime should be week endtime.
+     * @param {Object} reservation 
+     * @param {moment} endTime 
+     */
+    getReservationItem(reservation, displayDate) {
+        const reservationSpec = reservation.specifications_doc;
+        const group = moment.utc(reservation.start_time).format("MMM DD ddd");
+        const blockColor = RESERVATION_COLORS[this.getReservationType(reservationSpec.schedulability)];
+        let item = { id: `Res-${reservation.id}-${group}`,
+                        start_time: moment.utc(`${displayDate.format('YYYY-MM-DD')} ${reservation.start_time.format('HH:mm:ss')}`),
+                        end_time: moment.utc(`${displayDate.format('YYYY-MM-DD')} ${reservation.end_time.format('HH:mm:ss')}`),
+                        name: reservationSpec.activity.type, project: reservation.project_id,
+                        group: group,
+                        type: 'RESERVATION',
+                        title: `${reservationSpec.activity.type}${reservation.project_id?("-"+ reservation.project_id):""}`,
+                        desc: reservation.description,
+                        duration: reservation.duration?UnitConverter.getSecsToHHmmss(reservation.duration):"Unknown",
+                        bgColor: blockColor.bgColor, selectedBgColor: blockColor.bgColor, color: blockColor.color
+                    };
+        return item;
+    }
+
+    /**
+     * Get the schedule type from the schedulability object. It helps to get colors of the reservation blocks
+     * according to the type.
+     * @param {Object} schedulability 
+     */
+     getReservationType(schedulability) {
+        if (schedulability.manual && schedulability.dynamic) {
+            return 'true-true';
+        }   else if (!schedulability.manual && !schedulability.dynamic) {
+            return 'false-false';
+        }   else if (schedulability.manual && !schedulability.dynamic) {
+            return 'true-false';
+        }   else {
+            return 'false-true';
+        }
+    }
+
+    /**
+     * Set reservation filter
+     * @param {String} filter 
+     */
+    async setReservationFilter(filter) {
+        await this.setState({reservationFilter: filter});
+        let updatedItemGroupData = await this.dateRangeCallback(this.state.startTime, this.state.endTime, true);
+        this.timeline.updateTimeline(updatedItemGroupData);
+    }
+
     render() {
         if (this.state.redirect) {
             return <Redirect to={ {pathname: this.state.redirect} }></Redirect>
@@ -642,6 +808,28 @@ export class WeekTimelineView extends Component {
                                         <i className="pi pi-step-forward"></i>
                                     </button>
                                 </div> 
+                                <div className={`timeline-view-toolbar ${this.state.reservationEnabled && 'alignTimeLineHeader'}`}>
+                                    <div  className="sub-header">
+                                        <label >Show Reservations</label>
+                                        <InputSwitch checked={this.state.reservationEnabled} onChange={(e) => {this.showReservations(e)}} />                                       
+                                       
+                                    </div>
+                                
+                                    {this.state.reservationEnabled &&
+                                    <div className="sub-header">
+                                        <label style={{marginLeft: '20px'}}>Reservation</label>
+                                        <Dropdown optionLabel="name" optionValue="name" 
+                                                    style={{top:'2px'}}
+                                                    value={this.state.reservationFilter} 
+                                                    options={this.reservationReasons} 
+                                                    filter showClear={true} filterBy="name"
+                                                    onChange={(e) => {this.setReservationFilter(e.value)}} 
+                                                    placeholder="Reason"/>
+                                    
+                                    </div>
+                                    }
+                                </div>
+
                                 <Timeline ref={(tl)=>{this.timeline=tl}} 
                                         group={this.state.group} 
                                         items={this.state.items}
@@ -678,7 +866,7 @@ export class WeekTimelineView extends Component {
                 }
                 {/* SU Item Tooltip popover with SU status color */}
                 <OverlayPanel className="timeline-popover" ref={(el) => this.popOver = el} dismissable>
-                {mouseOverItem &&
+                {mouseOverItem  && mouseOverItem.type == "SCHEDULE" &&
                     <div className={`p-grid su-${mouseOverItem.status}`} style={{width: '350px'}}>
                         <label className={`col-5 su-${mouseOverItem.status}-icon`}>Project:</label>
                         <div className="col-7">{mouseOverItem.project}</div>
@@ -700,6 +888,37 @@ export class WeekTimelineView extends Component {
                         <div className="col-7">{mouseOverItem.duration}</div>
                     </div>
                 }
+                {(mouseOverItem && mouseOverItem.type == "RESERVATION") &&
+                    <div className={`p-grid`} style={{width: '350px', backgroundColor: mouseOverItem.bgColor, color: mouseOverItem.color}}>
+                        <h3 className={`col-12`}>Reservation Overview</h3>
+                        <hr></hr>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Name:</label>
+                        <div className="col-7">{mouseOverItem.name}</div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Description:</label>
+                        <div className="col-7">{mouseOverItem.desc}</div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Type:</label>
+                        <div className="col-7">{mouseOverItem.activity_type}</div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Stations:</label>
+                        {/* <div className="col-7"><ListBox options={mouseOverItem.stations} /></div> */}
+                        <div className="col-7 station-list">
+                            {mouseOverItem.stations.map((station, index) => (
+                                <div key={`stn-${index}`}>{station}</div>
+                            ))}
+                        </div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Project:</label>
+                        <div className="col-7">{mouseOverItem.project?mouseOverItem.project:"-"}</div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Start Time:</label>
+                        <div className="col-7">{mouseOverItem.displayStartTime.format(UIConstants.CALENDAR_DATETIME_FORMAT)}</div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>End Time:</label>
+                        <div className="col-7">{mouseOverItem.displayEndTime?mouseOverItem.displayEndTime.format(UIConstants.CALENDAR_DATETIME_FORMAT):'Unknown'}</div>
+                        {/* <label className={`col-5`} style={{color: mouseOverItem.color}}>Stations:</label>
+                        <div className="col-7">{mouseOverItem.stations.groups}:{mouseOverItem.stations.counts}</div> */}
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Duration:</label>
+                        <div className="col-7">{mouseOverItem.duration}</div>
+                        <label className={`col-5`} style={{color: mouseOverItem.color}}>Planned:</label>
+                        <div className="col-7">{mouseOverItem.planned?'Yes':'No'}</div>
+                    </div>
+                }
                 </OverlayPanel>
                 {/* Open Websocket after loading all initial data */}
                 {!this.state.isLoading &&
-- 
GitLab