From 14475882151b53d19bc97ada0158f6bdce76c85c Mon Sep 17 00:00:00 2001 From: Muthukrishnan <m.krishnan@redkarma.eu> Date: Fri, 18 Dec 2020 07:12:04 +0530 Subject: [PATCH] TMSS-407 - Reservation table implemented Reservation table implemented --- .../tmss_webapp/src/components/ViewTable.js | 201 +++++++++++++++++- .../src/layout/sass/reservation.scss | 3 + .../tmss_webapp/src/routes/Timeline/index.js | 3 +- .../tmss_webapp/src/routes/Timeline/list.js | 167 +++++++++++++++ .../tmss_webapp/src/routes/Timeline/view.js | 4 +- .../frontend/tmss_webapp/src/routes/index.js | 9 +- .../src/services/reservation.service.js | 35 +++ 7 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.js create mode 100644 SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 3428a67afc8..8206f04304b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -13,6 +13,9 @@ import {TriStateCheckbox} from 'primereact/tristatecheckbox'; import { Slider } from 'primereact/slider'; import { Button } from "react-bootstrap"; import { InputNumber } from "primereact/inputnumber"; +import {MultiSelect} from 'primereact/multiselect'; +import { RadioButton } from 'primereact/radiobutton'; +import {Dropdown} from 'primereact/dropdown'; let tbldata =[], filteredData = [] ; let selectedRows = []; @@ -24,7 +27,8 @@ let allowColumnSelection = true; let allowRowSelection = false; let columnclassname =[]; let parentCallbackFunction, parentCBonSelection; - +let cyclelist =[]; +let cycle= []; // Define a default UI for filtering function GlobalFilter({ preGlobalFilteredRows, @@ -114,6 +118,69 @@ function SelectColumnFilter({ ) } +// This is a custom filter UI for selecting +// a unique option from a list +function MultiSelectColumnFilter({ + column: { filterValue, setFilter, preFilteredRows, id }, +}) { + const [value, setValue] = useState(''); + //const [filtertype, setFiltertype] = useState(''); + React.useEffect(() => { + if (!filterValue && value) { + setValue(''); + } + }, [filterValue, value]); + const options = React.useMemo(() => { + let options = new Set(); + preFilteredRows.forEach(row => { + row.values[id].split(',').forEach( value => { + if ( value !== '') { + let hasValue = false; + options.forEach( option => { + if(option.name === value){ + hasValue = true; + } + }); + if(!hasValue) { + let option = { 'name': value, 'value':value}; + options.add(option); + } + } + }); + }); + return [...options.values()] + }, [id, preFilteredRows]) + // Render a multi-select box + return ( + <div onClick={e => { e.stopPropagation() }} > + {/*} <div className="p-field-radiobutton"> + <RadioButton inputId="filtertype1" name="filtertype" value="Any Of" onChange={(e) => this.setState({filtertype: e.value})} checked={this.state.filtertype === 'Any Of'} /> + <label htmlFor="filtertype1">Any Of</label> + </div> + <div className="p-field-radiobutton"> + <RadioButton inputId="filtertype2" name="filtertype" value="All" onChange={(e) => this.setState({filtertype: e.value})} checked={this.state.filtertype === 'All'} /> + <label htmlFor="filtertype2">All</label> + </div> */} + + <MultiSelect data-testid="stations" id="stations" optionLabel="value" optionValue="value" filter={true} + value={value} + options={options} + onChange={e => { setValue(e.target.value); + setFilter(e.target.value|| undefined) + }} + + style={{ + height: '24.2014px', + width: '60px', + border:'1px solid lightgrey', + }} + /> + + + </div> + ) +} + // This is a custom filter UI that uses a // slider to set the filter value between a column's // min and max values @@ -185,6 +252,115 @@ function CalendarColumnFilter({ ) } +// This is a custom filter UI that uses a +// calendar to set the value +function DateTimeColumnFilter({ + column: { setFilter, filterValue}, +}) { + // Calculate the min and max + // using the preFilteredRows + const [value, setValue] = useState(''); + React.useEffect(() => { + if (!filterValue && value) { + setValue(null); + } + }, [filterValue, value]); + return ( + + <div className="table-filter" onClick={e => { e.stopPropagation() }}> + <Calendar value={value} appendTo={document.body} onChange={(e) => { + const value = moment(e.value, moment.ISO_8601).format("YYYY-MMM-DD HH:mm:SS") + setValue(value); setFilter(value); + }} showIcon + // showTime= {true} + //showSeconds= {true} + // hourFormat= "24" + ></Calendar> + {value && <i onClick={() => {setFilter(undefined); setValue('') }} className="tb-cal-reset fa fa-times" />} + </div> + ) +} + +/** + * Custom function to filter data from date field. + * @param {Array} rows + * @param {String} id + * @param {String} filterValue + */ +function fromDatetimeFilterFn(rows, id, filterValue) { + const filteredRows = _.filter(rows, function(row) { + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" + let rowValue = moment.utc(row.values[id].split('.')[0]); + if (!rowValue.isValid()) { + // For cell data in format 'YYYY-MMM-DD' + rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + } + const start = moment.utc(moment(filterValue, 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + + return (start.isSameOrBefore(rowValue)); + } ); + return filteredRows; +} + +/** + * Custom function to filter data from date field. + * @param {Array} rows + * @param {String} id + * @param {String} filterValue + */ +function multiSelectFilterFn(rows, id, filterValue) { + const filteredRows = _.filter(rows, function(row) { + alert('tbldata',tbldata.length) + if ( filterValue.length === 0){ + return true; + } + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + + let rowValue = row.values[id]; + + let hasData = false; + filterValue.forEach(filter => { + if( rowValue.includes( filter )) { + hasData = true; + } + }); + return hasData; + } ); + return filteredRows; +} + +/** + * Custom function to filter data from date field. + * @param {Array} rows + * @param {String} id + * @param {String} filterValue + */ +function toDatetimeFilterFn(rows, id, filterValue) { + const filteredRows = _.filter(rows, function(row) { + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" + let rowValue = moment.utc(row.values[id].split('.')[0]); + if (!rowValue.isValid()) { + // For cell data in format 'YYYY-MMM-DD' + rowValue = moment.utc(moment(row.values[id], 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + } + const end = moment.utc(moment(filterValue, 'YYYY-MMM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + + return (end.isSameOrAfter(rowValue)); + } ); + return filteredRows; +} + /** * Custom function to filter data from date field. * @param {Array} rows @@ -328,6 +504,10 @@ const filterTypes = { 'select': { fn: SelectColumnFilter, }, + 'multiselect': { + fn: MultiSelectColumnFilter, + type: multiSelectFilterFn + }, 'switch': { fn: BooleanColumnFilter }, @@ -338,6 +518,14 @@ const filterTypes = { fn: CalendarColumnFilter, type: dateFilterFn }, + 'fromdatetime': { + fn: DateTimeColumnFilter, + type: fromDatetimeFilterFn + }, + 'todatetime': { + fn: DateTimeColumnFilter, + type: toDatetimeFilterFn + }, 'range': { fn: RangeColumnFilter, type: 'between' @@ -540,6 +728,15 @@ const defaultColumn = React.useMemo( </div> { showTopTotal && filteredData.length === data.length && <div className="total_records_top_label"> <label >Total records ({data.length})</label></div> + } + { cyclelist && cyclelist.length>0 && + <div className="total_records_top_label"> + <label >Cycles </label> + <MultiSelect data-testid="cycle" id="cycle" optionLabel="name" optionValue="url" filter={true} + value={cycle} + options={cyclelist} + /> + </div> } { showTopTotal && filteredData.length < data.length && <div className="total_records_top_label" ><label >Filtered {filteredData.length} from {data.length}</label></div>} @@ -629,6 +826,8 @@ function ViewTable(props) { const history = useHistory(); // Data to show in table tbldata = props.data; + cyclelist= props.cycleList; + parentCallbackFunction = props.filterCallback; parentCBonSelection = props.onRowSelection; isunittest = props.unittest; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss new file mode 100644 index 00000000000..19a02279d53 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/reservation.scss @@ -0,0 +1,3 @@ +.p-multiselect{ + height: 10px; +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js index b48cd64f554..67d78a0e432 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/index.js @@ -1,4 +1,5 @@ import {TimelineView} from './view'; import {WeekTimelineView} from './week.view'; +import { ReservationList} from './list'; -export {TimelineView, WeekTimelineView} ; +export {TimelineView, WeekTimelineView, ReservationList} ; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.js new file mode 100644 index 00000000000..0aeaf573046 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.js @@ -0,0 +1,167 @@ +import React, {Component} from 'react'; +import ReservationService from './../../services/reservation.service'; +import AppLoader from "./../../layout/components/AppLoader"; +import ViewTable from './../../components/ViewTable'; +import PageHeader from '../../layout/components/PageHeader'; +import CycleService from '../../services/cycle.service'; +import _ from 'lodash'; +import moment from 'moment'; + +export class ReservationList extends Component{ + constructor(props){ + super(props); + this.state = { + reservationsList: [], + defaultcolumns: [{ + name:"System Id", + description:"Description", + start_time: { + name: "Start Time", + filter: "fromdatetime" + }, + end_time: { + name: "End Time", + filter: "todatetime" + }, + duration:"Duration (HH:mm:ss)", + type: "Reservation type", + subject: "Subject", + planned:"Planned", + stations:{ + name:"Stations", + filter:"multiselect" + }, + /* cycles:{ + name:"Cycle Id", + filter:"select" + },*/ + manual: "Manual", + dynamic: "Dynamic", + project_id: "Project", + expert: "Expert", + hba_rfi: "HBA-RFI", + lba_rfi: "LBA-RFI" + }], + optionalcolumns: [{ + actionpath:"actionpath" + }], + columnclassname: [{ + "Duration (HH:mm:ss)":"filter-input-75", + "Reservation type":"filter-input-100", + "Subject":"filter-input-50", + "Planned":"filter-input-50", + "Stations":"filter-input-150", + "Manual":"filter-input-50", + "Dynamic":"filter-input-50", + "Expert":"filter-input-50", + "HBA-RFI":"filter-input-50", + "LBA-RFI":"filter-input-50", + + }], + defaultSortColumn: [{id: "System Id", desc: false}], + isLoading: true, + cycleList: [], + } + this.reservations= []; + this.cycleList= []; + this.sortListCycle= []; + } + + async componentDidMount() { + const promises = [ ReservationService.getAllReservation(), + CycleService.getAllCycles(), + ]; + + this.reservations = []; + await Promise.all(promises).then(responses => { + let reservation = {}; + const cycleList = responses[1]; + for( const response of responses[0]){ + reservation = response; + reservation = this.mergeResourceWithReservation( reservation, response.specifications_doc.activity) ; + reservation = this.mergeResourceWithReservation( reservation, response.specifications_doc.effects ); + reservation = this.mergeResourceWithReservation( reservation, response.specifications_doc.schedulability ); + if (response.specifications_doc.resources.stations ) { + reservation['stations'] = response.specifications_doc.resources.stations.join(); + } else { + reservation['stations'] = ''; + } + //Get associated cycle list + reservation['cycles'] = this.getCycleByProject(reservation.project_id, cycleList).join(); + if(reservation.duration === null || reservation.duration === ''){ + reservation.duration = 'Unknown'; + reservation['end_time']= 'Unknown'; + } else { + reservation.duration = moment.utc((reservation.duration || 0)*1000).format('HH:mm:ss'); + let endDate = moment(reservation.start_time); + endDate.add(Number(reservation.duration), 'seconds'); + reservation['end_time']= moment(endDate).format('YYYY-MM-DD HH:mm:ss'); + } + + + reservation['start_time']= moment(reservation.start_time).format('YYYY-MM-DD HH:mm:ss'); + this.reservations.push(reservation); + }; + + //specifications_doc.resources + this.setState({ + isLoading: false, + reservationsList: this.reservations, + cycleList: this.sortListCycle, + }); + }); + } + + getCycleByProject (name, cycleList) { + let cycleName= []; + cycleList.map(cycle => { + if (_.includes( cycle.projects_ids, name)) { + cycleName.push(cycle.name); + const tmpCycleList = _.filter(this.sortListCycle, function(o) { return o.name === cycle.name }); + if (tmpCycleList.length === 0) { + this.sortListCycle.push(cycle); + } + } + + }); + return cycleName; + } + + mergeResourceWithReservation ( reservation, params) { + if( params ){ + Object.keys(params).map((key, i) => ( + reservation[key]= params[key] + )); + } + return reservation; + } + + render() { + + return ( + <React.Fragment> + <PageHeader location={this.props.location} title={'Reservation - List'} + actions={[]} + /> + {this.state.isLoading? <AppLoader /> : (this.state.reservationsList && this.state.reservationsList.length>0) ? + <ViewTable + data={this.state.reservationsList} + defaultcolumns={this.state.defaultcolumns} + optionalcolumns={this.state.optionalcolumns} + columnclassname={this.state.columnclassname} + defaultSortColumn={this.state.defaultSortColumn} + showaction="true" + paths={this.state.paths} + keyaccessor="name" + unittest={this.state.unittest} + tablename="reservation_list" + cycleList={this.state.cycleList} + /> + : <div>No Reservation found </div> + + } + </React.Fragment> + ); + } + +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js index 9a0b6cc381d..9dec851bd66 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -326,7 +326,9 @@ export class TimelineView extends Component { return ( <React.Fragment> <PageHeader location={this.props.location} title={'Scheduling Units - Timeline View'} - actions={[{icon: 'fa-calendar-alt',title:'Week View', props : { pathname: `/su/timelineview/week`}}]}/> + actions={[ + {icon: 'fa-plus-square',title:'Reservation List', props : { pathname: `/su/timelineview/reservation/list`}}, + {icon: 'fa-calendar-alt',title:'Week View', props : { pathname: `/su/timelineview/week`}}]}/> { this.state.isLoading ? <AppLoader /> : <div className="p-grid"> {/* SU List Panel */} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index 156267acb0f..c5125cf8d2b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -14,7 +14,7 @@ import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' import SchedulingUnitCreate from './Scheduling/create'; import EditSchedulingUnit from './Scheduling/edit'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; -import {TimelineView, WeekTimelineView} from './Timeline'; +import {ReservationList, TimelineView, WeekTimelineView} from './Timeline'; import SchedulingSetCreate from './Scheduling/create.scheduleset'; import Workflow from './Workflow'; @@ -159,7 +159,12 @@ export const routes = [ name: 'Workflow', title: 'QA Reporting (TO)' }, - + { + path: "/su/timelineview/reservation/list", + component: ReservationList, + name: 'Reservation List', + title:'Reservation List' + }, ]; export const RoutedContent = () => { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js new file mode 100644 index 00000000000..78dc3f49759 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/reservation.service.js @@ -0,0 +1,35 @@ +const axios = require('axios'); + +axios.defaults.headers.common['Authorization'] = 'Basic dGVzdDp0ZXN0'; + +const ReservationService = { + getReservation: async function () { + try { + const url = `/api/reservation_template`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, + saveReservation: async function (reservation) { + try { + const response = await axios.post(('/api/reservation/'), reservation); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + getAllReservation: async function () { + try { + const url = `/api/reservation`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error(error); + } + }, +} + +export default ReservationService; -- GitLab