diff --git a/SAS/TMSS/frontend/tmss_webapp/.vscode/launch.json b/SAS/TMSS/frontend/tmss_webapp/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..f6b35a0b639d037c20e30a924f2df13e783620c5 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index 705a5183e8801882a780f93350b30333e5bd3582..c32b29542e63179bd7481faa6b90dfa4b122b27c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -22,6 +22,8 @@ "font-awesome": "^4.7.0", "history": "^5.0.0", "interactjs": "^1.9.22", + "js-cookie": "^2.2.1", + "katex": "^0.12.0", "lodash": "^4.17.19", "match-sorter": "^4.1.0", "moment": "^2.27.0", @@ -57,7 +59,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, - "proxy": "http://127.0.0.1:8008/", + "proxy": "http://localhost:8008/", "eslintConfig": { "extends": "react-app" }, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 28dc4939d93ef1dcc6a112bb1da40a4a8fa79067..3abca18eb4682b9a8debe753161544e2b4d0c0ea 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import { Redirect} from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom'; import classNames from 'classnames'; import {AppTopbar} from './layout/components/AppTopbar'; @@ -15,40 +16,41 @@ import './layout/layout.scss'; import 'primeflex/primeflex.css'; import './App.scss'; import './App.css'; +import Auth from'./authenticate/auth'; +import { Login } from './authenticate/login'; class App extends Component { constructor() { - super(); - this.state = { - layoutMode: 'static', - currentMenu: '', - currentPath: '/', - PageTitle:'', - staticMenuInactive: localStorage.getItem('staticMenuInactive') === 'true' ? true : false, - overlayMenuActive: localStorage.getItem('overlayMenuActive') === 'true' ? true : false, - mobileMenuActive: localStorage.getItem('mobileMenuActive') === 'true' ? true : false, - }; - this.onWrapperClick = this.onWrapperClick.bind(this); + super(); + this.state = { + layoutMode: 'static', + currentMenu: '', + currentPath: '/', + PageTitle:'', + staticMenuInactive: localStorage.getItem('staticMenuInactive') === 'true' ? true : false, + overlayMenuActive: localStorage.getItem('overlayMenuActive') === 'true' ? true : false, + mobileMenuActive: localStorage.getItem('mobileMenuActive') === 'true' ? true : false, + authenticated: Auth.isAuthenticated(), + redirect: (Auth.isAuthenticated() && window.location.pathname === "/login")?"/":window.location.pathname + }; + this.onWrapperClick = this.onWrapperClick.bind(this); this.onToggleMenu = this.onToggleMenu.bind(this); this.onSidebarClick = this.onSidebarClick.bind(this); this.onMenuItemClick = this.onMenuItemClick.bind(this); this.setPageTitle = this.setPageTitle.bind(this); - - this.menu = [ - {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard',section: 'dashboard'}, - {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle',section: 'cycle'}, - {label: 'Project', icon: 'fab fa-fw fa-wpexplorer', to:'/project',section: 'project'}, - {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit',section: 'schedulingunit'}, - {label: 'Timeline', icon: 'pi pi-fw pi-clock', to:'/su/timelineview',section: 'su/timelineview'}, - // {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, - - - ]; + this.loggedIn = this.loggedIn.bind(this); + this.logout = this.logout.bind(this); - // this.menuComponent = {'Dashboard': Dashboard} - } - - onWrapperClick(event) { + this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard',section: 'dashboard'}, + {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle',section: 'cycle'}, + {label: 'Project', icon: 'fab fa-fw fa-wpexplorer', to:'/project',section: 'project'}, + {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit',section: 'schedulingunit'}, + {label: 'Timeline', icon: 'pi pi-fw pi-clock', to:'/su/timelineview',section: 'su/timelineview'}, + // {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, + ]; + } + + onWrapperClick(event) { if (!this.menuClick) { this.setState({ overlayMenuActive: false, @@ -94,9 +96,9 @@ class App extends Component { this.menuClick = true; } - onMenuItemClick(event) { - this.setState({currentMenu:event.item.label, currentPath: event.item.path}); - } + onMenuItemClick(event) { + this.setState({currentMenu:event.item.label, currentPath: event.item.path}); + } isDesktop() { return window.innerWidth > 1024; @@ -107,36 +109,72 @@ class App extends Component { this.setState({ PageTitle }) } } - - render() { - const wrapperClass = classNames('layout-wrapper', { - 'layout-overlay': this.state.layoutMode === 'overlay', - 'layout-static': this.state.layoutMode === 'static', - 'layout-static-sidebar-inactive': this.state.staticMenuInactive && this.state.layoutMode === 'static', - 'layout-overlay-sidebar-active': this.state.overlayMenuActive && this.state.layoutMode === 'overlay', - 'layout-mobile-sidebar-active': this.state.mobileMenuActive - }); - const AppBreadCrumbWithRouter = withRouter(AppBreadcrumb); - - return ( - <React.Fragment> - <div className="App"> - {/* <div className={wrapperClass} onClick={this.onWrapperClick}> */} - <div className={wrapperClass}> - <AppTopbar onToggleMenu={this.onToggleMenu}></AppTopbar> - <Router basename={ this.state.currentPath }> - <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} layoutMode={this.state.la} active={this.state.menuActive}/> - <div className="layout-main"> - <AppBreadCrumbWithRouter setPageTitle={this.setPageTitle} /> - <RoutedContent /> - </div> - </Router> - <AppFooter></AppFooter> - </div> + + /** + * Callback function from login page to set the authentication state to true amd redirect to the + * original requested URL. + */ + loggedIn() { + const redirect = this.state.redirect; + this.setState({authenticated: true, redirect: redirect==="/login"?"/":redirect}); + } + + /** + * Logout and redirect to login page. + */ + logout() { + Auth.logout(); + this.setState({authenticated: false, redirect:"/"}); + } + + render() { + const wrapperClass = classNames('layout-wrapper', { + 'layout-overlay': this.state.layoutMode === 'overlay', + 'layout-static': this.state.layoutMode === 'static', + 'layout-static-sidebar-inactive': this.state.staticMenuInactive && this.state.layoutMode === 'static', + 'layout-overlay-sidebar-active': this.state.overlayMenuActive && this.state.layoutMode === 'overlay', + 'layout-mobile-sidebar-active': this.state.mobileMenuActive + }); + const AppBreadCrumbWithRouter = withRouter(AppBreadcrumb); + console.log(this.props); + return ( + <React.Fragment> + <div className="App"> + {/* <div className={wrapperClass} onClick={this.onWrapperClick}> */} + <div className={wrapperClass}> + + {/* Load main routes and application only if the application is authenticated */} + {this.state.authenticated && + <> + <AppTopbar onToggleMenu={this.onToggleMenu} isLoggedIn={this.state.authenticated} onLogout={this.logout}></AppTopbar> + <Router basename={ this.state.currentPath }> + <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} layoutMode={this.state.la} active={this.state.menuActive}/> + <div className="layout-main"> + {this.state.redirect && + <Redirect to={{pathname: this.state.redirect}} />} + <AppBreadCrumbWithRouter setPageTitle={this.setPageTitle} /> + <RoutedContent /> + </div> + </Router> + <AppFooter></AppFooter> + </> + } + + {/* If not authenticated, show only login page */} + {!this.state.authenticated && + <> + <Router basename={ this.state.currentPath }> + <Redirect to={{pathname: "/login"}} /> + <Login onLogin={this.loggedIn} /> + </Router> + </> + } + + </div> </div> - </React.Fragment> - ); - } + </React.Fragment> + ); + } } export default App; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/authenticate/auth.js b/SAS/TMSS/frontend/tmss_webapp/src/authenticate/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..c5e845a6f3d5b5433750ad193d8dc2ce628ef466 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/authenticate/auth.js @@ -0,0 +1,37 @@ +// import AuthService from "../services/auth.service"; + +/** + * Global functions to authenticate user and get user details from browser local storage. + */ +const Auth = { + /** To check if user already logged in and the token is available in the browser local storage */ + isAuthenticated: () => { + let user = localStorage.getItem("user"); + if (user) { + user = JSON.parse(user); + if (user.token) { + return true; + } + } + return false; + }, + /** Gets user details from browser local storage */ + getUser: () => { + return JSON.parse(localStorage.getItem("user")); + }, + /** Authenticate user from the backend and store user details in local storage */ + login: async(user, pass) => { + // const user = await AuthService.authenticate(); + if (user) { + //TODO set token and username + } + localStorage.setItem("user", JSON.stringify({name:user, token: "ABCDEFGHIJ"})); + return true; + }, + /** Remove user details from localstorage on logout */ + logout: () => { + localStorage.removeItem("user"); + } +} + +export default Auth; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js b/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js new file mode 100644 index 0000000000000000000000000000000000000000..4512989fd19744d8476290430ad3a77819e656c5 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/authenticate/login.js @@ -0,0 +1,133 @@ +import { InputText } from 'primereact/inputtext'; +import React, {Component} from 'react'; +import { Redirect } from 'react-router-dom'; +import Auth from '../authenticate/auth'; + +/** + * Component to authenticate users in the application. + */ +export class Login extends Component { + constructor(props){ + super(props); + this.state = { + username: null, + password: null, + redirect: Auth.isAuthenticated()?"/":null, //If already logged in, redirect to home page + error: null, + errors: {}, + validFields: {} + }; + this.login = this.login.bind(this); + this.setCredentials = this.setCredentials.bind(this); + } + + /** + * To set form field values. + * @param {String} field + * @param {String} value + */ + setCredentials(field, value) { + let state = this.state; + let errors = state.errors; + let validFields = state.validFields; + // If value is null or empty set error field and remove from valid field and vice versa + if (!value) { + errors[field] = `${field} required`; + delete validFields[field]; + } else { + delete errors[field]; + validFields[field] = field; + } + state[field] = value; + state.errors = errors; + state.validFields = validFields; + this.setState(state); + } + + /** + * Login function called on click of 'Login' button. + * If authenticated, callback parent component function. + */ + async login() { + const loggedIn = await Auth.login(this.state.username, this.state.password); + if (loggedIn) { + this.setState({error: false}); + this.props.onLogin(); + } else { + this.setState({error: true}); + } + } + + render() { + if (this.state.redirect) { + return (<Redirect to={{pathname: this.state.redirect}} />); + } + return ( + <> + <div className="container-fluid bg-login"> + <div className="container"> + <div className="row"> + <div className="col-lg-9 col-md-12 login-card"> + <div className="row"> + {/* Left panel with image and TMSS title */} + <div className="col-md-5 detail-part"> + <h3>Telescope Manager Specification System</h3> + <p>By ASTRON</p> + </div> + {/* Right panel with login form */} + <div className="col-md-7 logn-part"> + <div className="row"> + <div className="col-lg-10 col-md-12 mx-auto"> + <div className="logo-cover"> + {/* <img src="./logo.png" alt="" /> */} + </div> + <div className="login-form"> + <h4>Login</h4> + <div className="form-field"> + <span className="p-float-label"> + <InputText id="inputtext" className={`${this.state.errors.username?"input-error ":""} form-control`} + value={this.state.username} onChange={(e) => this.setCredentials('username', e.target.value)} /> + <label htmlFor="inputtext"><i className="fa fa-user"></i>Enter Username</label> + </span> + <label className={this.state.errors.username?"error":""}> + {this.state.errors.username?this.state.errors.username : ""} + </label> + </div> + <div className="form-field"> + <span className="p-float-label"> + <InputText id="inputtext" className={`${this.state.errors.password?"input-error ":""} form-control`} + type="password" value={this.state.password} onChange={(e) => this.setCredentials('password', e.target.value )} /> + <label htmlFor="inputtext"><i className="fa fa-key"></i>Enter Password</label> + </span> + <label className={this.state.errors.password?"error":""}> + {this.state.errors.password?this.state.errors.password : ""} + </label> + </div> + <div className="row form-footer"> + <div className="col-md-6 forget-paswd"> + + </div> + <div className="col-md-6 button-div"> + <button className="btn btn-primary" + disabled={Object.keys(this.state.validFields).length<2} + onClick={this.login}>Login</button> + </div> + </div> + {this.state.error && + <div className="row error"> + Unable to login, please try with different Username and/or Password. + </div> + } + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..644620bc7655a8ab4a00b9e3e2ee1aff7d5e44bf --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenEditor.js @@ -0,0 +1,172 @@ +import React, { Component } from 'react'; + +import {Calendar} from 'primereact/calendar'; +import { Dialog } from 'primereact/dialog'; +import { Button } from 'primereact/button'; + +import moment from 'moment'; +import _ from 'lodash'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +export default class BetweenEditor extends Component { + constructor(props) { + super(props); + this.tmpRowData = []; + + this.state = { + showDialog: false, + dialogTitle: '', + }; + + this.copyDateValue = this.copyDateValue.bind(this); + } + + isPopup() { + return true; + } + + /** + * Init the date value if exists + */ + async componentDidMount(){ + let parentRows = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; + let parentCellData = parentRows[this.props.colDef.field]; + this.tmpRowData = []; + if(parentCellData){ + let cellDataArray = _.split(parentCellData, '|'); + await cellDataArray.forEach(dataVal =>{ + let line = {}; + let dataValue = _.split(dataVal, ','); + line['from'] = (dataValue[0])? moment(dataValue[0]).toDate():''; + line['until'] = ( dataValue[1])? moment(dataValue[1]).toDate():''; + this.tmpRowData.push(line); + }); + } + if(this.tmpRowData.length>0){ + let row = this.tmpRowData[this.tmpRowData.length-1]; + if((row['from'] !== '' && row['from'] !== 'undefined') && (row['until'] !== '' && row['until'] !== 'undefined')){ + let line = {'from': '', 'until': ''}; + this.tmpRowData.push(line); + } + }else{ + let line = {'from': '', 'until': ''}; + this.tmpRowData.push(line); + } + + await this.setState({ + rowData: this.tmpRowData, + dialogTitle: (this.props.colDef.field === 'between') ? this.props.colDef.field : 'Not-Between', + showDialog: true, + }); + + } + + /*isCancelAfterEnd(){console.log('after') + console.log('called') + this.copyDateValue(); + }*/ + + /** + * Call the function on click Esc or Close the dialog + */ +async copyDateValue(){ + let consolidateDates = ''; + this.state.rowData.map(row =>{ + if((row['from'] !== '' && row['from'] !== 'undefined') && (row['until'] !== '' && row['until'] !== 'undefined')){ + consolidateDates += ((row['from'] !== '')?moment(row['from']).format(DATE_TIME_FORMAT):'' )+","+((row['until'] !== '')?moment(row['until']).format(DATE_TIME_FORMAT):'')+"|"; + } + }); + await this.props.context.componentParent.updateTime( + this.props.node.rowIndex,this.props.colDef.field, consolidateDates + ); + this.setState({ showDialog: false}); + +} + +/* + Set value in relevant field + */ +updateDateChanges(rowIndex, field, e){ + let tmpRows = this.state.rowData; + let row = tmpRows[rowIndex]; + row[field] = e.value; + tmpRows[rowIndex] = row; + if(this.state.rowData.length === rowIndex+1){ + let line = {'from': '', 'until': ''}; + tmpRows.push(line); + } + this.setState({ + rowData: tmpRows + }) +} + +/* + Remove the the row from dialog +*/ +removeInput(rowIndex){ + let tmpRows = this.state.rowData; + delete tmpRows[rowIndex]; + this.setState({ + rowData: tmpRows + }) +} + +render() { + return ( + <> + {this.state.rowData && this.state.rowData.length > 0 && + <Dialog header={_.startCase(this.state.dialogTitle)} visible={this.state.showDialog} maximized={false} + onHide={() => {this.copyDateValue()}} inputId="confirm_dialog" + footer={<div> + <Button key="back" label="Close" onClick={() => {this.copyDateValue()}} /> + </div> + } > + <div className="ag-theme-balham" style={{ height: '500px', width: '600px', paddingLeft: '20px' }}> + <div className="p-field p-grid" > + <React.Fragment> + <label key={'labelFrom'} className="col-lg-6 col-md-6 col-sm-12">From</label> + <label key={'labelUntil'} className="col-lg-4 col-md-5 col-sm-12">Until</label> + <label key={'labelRemove'} className="col-lg-2 col-md-2 col-sm-12">Remove</label> + </React.Fragment> + </div> + {this.state.rowData.map((bdate, index) => ( + <React.Fragment key={index}> + <div className="p-field p-grid" > + <Calendar + d dateFormat="dd-M-yy" + value= {this.state.rowData[index].from} + onChange= {e => {this.updateDateChanges(index, 'from', e)}} + // onBlur= {e => {this.updateDateChanges(index, 'from', e)}} + showTime={true} + showSeconds={true} + hourFormat="24" + showIcon={true} + /> + <Calendar + d dateFormat="dd-M-yy" + value= {this.state.rowData[index].until} + onChange= {e => {this.updateDateChanges(index, 'until', e)}} + // onBlur= {e => {this.updateDateChanges(index, 'until', e)}} + showTime={true} + showSeconds={true} + hourFormat="24" + showIcon={true} + style={{marginLeft:'60px'}} + /> + {this.state.rowData.length !== (index+1) && + <button className="p-link" style={{marginLeft: '6vw'}} onClick={(e) => this.removeInput(index)} > + <i className="fa fa-trash pi-error"></i></button> + } + </div> + + </React.Fragment> + ))} + </div> + </Dialog> + } + </> + ); +} + +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js new file mode 100644 index 0000000000000000000000000000000000000000..90a8ca3d7fc4ca9f22084fd5c9e6db063e80366d --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/BetweenRenderer.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react'; + +export default class BetweenRenderer extends Component { + constructor(props) { + super(props); + } + + /** + Show cell value in grid + */ + render() { + let row = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; + let value = row[this.props.colDef.field]; + return <> {value && + value + }</>; + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComp.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComp.js new file mode 100644 index 0000000000000000000000000000000000000000..e92fe5f4719adac8aebd00fcc41aa4b5cfa5e444 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComp.js @@ -0,0 +1,85 @@ +import React, { Component } from 'react'; +import {Calendar} from 'primereact/calendar'; +import moment from 'moment'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +export default class CustomDateComp extends Component { + constructor(props) { + super(props); + this.state = { + date: '', + }; + } + + componentDidMount(){ + let parentRows = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; + let parentCellData = parentRows[this.props.colDef.field]; + this.setState({ + date:parentCellData + }) + } + + isPopup() { + return true; + } + + isCancelAfterEnd(){ + let date = (this.state.date !== '' && this.state.date !== 'undefined')? moment(this.state.date).format(DATE_TIME_FORMAT) :''; + this.props.context.componentParent.updateTime( + this.props.node.rowIndex,this.props.colDef.field, date + ); + } + + render() { + return ( + <Calendar + d dateFormat="dd-M-yy" + value= {this.state.date} + onChange= {e => {this.updateDateChanges(e)}} + onBlur= {e => {this.updateDateChanges(e)}} + //data-testid="start" + showTime= {true} + showSeconds= {true} + hourFormat= "24" + showIcon= {true} + /> + ); + } + + + updateDateChanges(e){ + if(e.value){ + this.setState({date : e.value}); + } + } + + ondatechange(e){ + this.setState({date : e.value}); + } + + getDate() { + return this.state.date; + } + + setDate(date) { + this.setState({ date }); + this.picker.setDate(date); + } + + updateAndNotifyAgGrid(date) { + this.setState( + { + date, + }, + this.props.onDateChanged + ); + } + + + onDateChanged = (selectedDates) => { + this.props.context.componentParent.updateTime( + this.props.node.rowIndex,this.props.colDef.field,selectedDates[0] + ); + }; +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..7e0c18e9b6926bb138c3c6b7667d67f7fa76d930 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/CustomDateComponent.js @@ -0,0 +1,114 @@ +import React, { Component } from 'react'; +import flatpickr from 'flatpickr'; +import "flatpickr/dist/flatpickr.css"; + +export default class CustomDateComponent extends Component { + constructor(props) { + super(props); + + this.state = { + date: null, + }; + } + + isPopup() { + return true; + } + + render() { + //Inlining styles to make simpler the component + return ( + <div + className="ag-input-wrapper custom-date-filter" + role="presentation" + ref="flatpickr" + style={{ height: '50px', width: '90%' }}> + + <input type="text" ref="eInput" data-input style={{ width: '100%',}} /> + <a className="input-button" title="clear" data-clear> + <i className="fa fa-times"></i> + </a> + </div> + ); + } + + componentDidMount() { + this.picker = flatpickr(this.refs.flatpickr, { + onChange: this.onDateChanged.bind(this), + dateFormat: 'd-M-Y', + timeFormat: "h:m:d", + wrap: true, + showClearButton: false, + inlineHideInput: true, + defaultHour: 0, + defaultMinute: 1, + enableSeconds: true, + defaultSecond: 0, + hourIncrement: 1, + minuteIncrement: 1, + secondIncrement: 5, + time_24hr: true, + allowInput: true + }); + + this.eInput = this.refs.eInput; + + this.picker.calendarContainer.classList.add('ag-custom-component-popup'); + } + + //********************************************************************************* + // METHODS REQUIRED BY AG-GRID + //********************************************************************************* + + getDate() { + //ag-grid will call us here when in need to check what the current date value is hold by this + //component. + return this.state.date; + } + + setDate(date) { + //ag-grid will call us here when it needs this component to update the date that it holds. + this.setState({ date }); + this.picker.setDate(date); + } + + //********************************************************************************* + // AG-GRID OPTIONAL METHODS + //********************************************************************************* + + setInputPlaceholder(placeholder) { + this.eInput.setAttribute('placeholder', placeholder); + } + + setInputAriaLabel(label) { + this.eInput.setAttribute('aria-label', label); + } + + //********************************************************************************* + // LINKS THE INTERNAL STATE AND AG-GRID + //********************************************************************************* + + updateAndNotifyAgGrid(date) { + this.setState( + { + date, + }, + //Callback after the state is set. This is where we tell ag-grid that the date has changed so + //it will proceed with the filtering and we can then expect ag-Grid to call us back to getDate + this.props.onDateChanged + ); + } + + //********************************************************************************* + // LINKING THE UI, THE STATE AND AG-GRID + //********************************************************************************* + onDateChanged = (selectedDates) => { + //console.log('>>', selectedDates[0]) + this.props.context.componentParent.updateTime( + this.props.node.rowIndex,this.props.colDef.field,selectedDates[0] + ); + + + // this.updateAndNotifyAgGrid(selectedDates[0]); + }; +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/DegreeInputmask.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/DegreeInputmask.js index 151b9edb3ccc07af675b1ab0bdae39d96bc3d38e..320f815503edfbd9d8f203daaa97f21c84ab9af0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/DegreeInputmask.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/DegreeInputmask.js @@ -2,19 +2,25 @@ import React, { Component } from 'react'; import { InputMask } from 'primereact/inputmask'; import Validator from '../../utils/validator'; +const BG_COLOR= '#f878788f'; + export default class DegreeInputMask extends Component { constructor(props) { super(props); this.callbackUpdateAngle = this.callbackUpdateAngle.bind(this); } + /** + * Update Angle value + * @param {*} e + */ callbackUpdateAngle(e) { let isValid = false; if(Validator.validateAngle(e.value)){ e.originalEvent.target.style.backgroundColor = ''; isValid = true; }else{ - e.originalEvent.target.style.backgroundColor = 'orange'; + e.originalEvent.target.style.backgroundColor = BG_COLOR; } this.props.context.componentParent.updateAngle( this.props.node.rowIndex,this.props.colDef.field,e.value,false,isValid diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js new file mode 100644 index 0000000000000000000000000000000000000000..c6df1658da13b941ace35509c758b001731f6f2e --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/MultiSelector.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import {MultiSelect} from 'primereact/multiselect'; +import _ from 'lodash'; + +export default class SkySllector extends Component { + constructor(props) { + super(props); + + this.dailyOptions= [ + {name: 'require_day', value: 'require_day'}, + {name: 'require_night', value: 'require_night'}, + {name: 'avoid_twilight', value: 'avoid_twilight'}, + ]; + this.state= { + daily: [], + + } + + this.callbackUpdateDailyCell = this.callbackUpdateDailyCell.bind(this); + } + + async componentDidMount(){ + let selectedValues = this.props.data['daily']; + if(selectedValues && selectedValues.length>0){ + let tmpDailyValue = _.split(selectedValues, ","); + await this.setState({ + daily: tmpDailyValue, + }); + } + + console.log('this.props.props',this.props.data['daily'], this.state.daily) + + // this.props.props. + /* console.log('---',this.props.data['daily']) + await this.setState({ + daily: this.props.data['daily'] + })*/ + } + + async callbackUpdateDailyCell(e) { + let isValid = false; + this.setState({ + daily: e.value + }) + let dailyValue = ''; + let selectedValues = e.value; + await selectedValues.forEach( key =>{ + dailyValue += key+","; + }) + dailyValue = _.trim(dailyValue) + dailyValue = dailyValue.replace(/,([^,]*)$/, '' + '$1') + + this.props.context.componentParent.updateDailyCell( + this.props.node.rowIndex,this.props.colDef.field,dailyValue + ); + + } + + afterGuiAttached(){ + // this.input.input.focus(); + } + isPopup() { + return true; + } + render() { + return ( + <div className="col-sm-6"> + <MultiSelect optionLabel="name" value={this.state.daily} options={this.dailyOptions} + optionLabel="value" optionValue="value" filter={true} + onChange={this.callbackUpdateDailyCell} /> + </div> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/RenderTimeInputmask.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/RenderTimeInputmask.js deleted file mode 100644 index c11bfe84597cf6d471a1cc8653be5556f5eccf64..0000000000000000000000000000000000000000 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/RenderTimeInputmask.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react'; -import { InputMask } from 'primereact/inputmask'; -import Validator from '../../utils/validator'; - -export default class RenderTimeInputmask extends Component{ - constructor(props) { - super(props); - this.callbackUpdateAngle = this.callbackUpdateAngle.bind(this); - } - - callbackUpdateAngle(e) { - let isValid = false; - if(Validator.validateTime(e.value)){ - e.originalEvent.target.style.backgroundColor = ''; - isValid = true; - }else{ - e.originalEvent.target.style.backgroundColor = 'orange'; - } - - this.props.context.componentParent.updateAngle( - this.props.node.rowIndex,this.props.colDef.field,e.value,false,isValid - ); - - } - - /* - isPopup(){} - isCancelBeforeStart(){} - - focusIn(){} - focusOut(){} - destroy(){} - */ - - isCancelAfterEnd(){ - // console.log('params', this.props); - - // return false; - } - afterGuiAttached(){ - //this.input.input.focus(); - } - - - getValue(){ - // console.log(this.input.value) - } - render() { - return ( - <InputMask - value={this.props.value} - mask="99:99:99" - placeholder="HH:mm:ss" - className="inputmask" - onComplete={this.callbackUpdateAngle} - ref={input =>{this.input = input}} - /> - ); - } -} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..66aa263f2c1e43af512d069dbfa176df8d31fa3b --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/StationEditor.js @@ -0,0 +1,181 @@ +import React, { Component } from 'react'; + +import { Dialog } from 'primereact/dialog'; +import { Button } from 'primereact/button'; +import Stations from '../../routes/Scheduling/Stations'; + +import moment from 'moment'; +import _ from 'lodash'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +export default class StationEditor extends Component { + constructor(props) { + super(props); + this.tmpRowData = []; + + this.state = { + schedulingUnit: {}, + showDialog: false, + dialogTitle: 'Station Group', + missingStationFieldsErrors: [], + stationGroup: [], + customSelectedStations: [] + }; + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + }; + } + + isPopup() { + return true; + } + + /** + * Init the date value if exists + */ + async componentDidMount(){ + let tmpStationGroups = []; + let tmpStationGroup = {}; + + let rowSU = this.props.agGridReact.props.rowData[this.props.node.rowIndex]; + let sgCellValue = rowSU[this.props.colDef.field]; + + if(sgCellValue && sgCellValue.length >0){ + let stationGroups = _.split(sgCellValue, "|"); + stationGroups.map(stationGroup =>{ + tmpStationGroup = {}; + let sgValue = _.split(stationGroup, ":"); + if(sgValue && sgValue[0].length>0){ + let stationArray = _.split(sgValue[0], ","); + + tmpStationGroup['stations'] = stationArray; + tmpStationGroup['max_nr_missing'] = sgValue[1]; + tmpStationGroups.push(tmpStationGroup); + } + + }) + this.setState({ + stationGroup: tmpStationGroups, + showDialog: true + }); + }else{ + let defaultSGs = this.props.context.componentParent.state.defaultStationGroups; + if(defaultSGs){ + this.setState({ + stationGroup: defaultSGs, + selectedStations: defaultSGs, + showDialog: true + }); + } + } + } + +validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.schedulingUnit[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.schedulingUnit[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + this.setState({errors: errors, validFields: validFields}); + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + return validForm && !this.state.missingStationFieldsErrors; +} + +async updateStationGroup(){ + let stationValue = ''; + const station_groups = []; + (this.state.selectedStations || []).forEach(key => { + let station_group = {}; + const stations = this.state[key] ? this.state[key].stations : []; + const max_nr_missing = parseInt(this.state[key] ? this.state[key].missing_StationFields : 0); + station_group = { + stations, + max_nr_missing + }; + station_groups.push(station_group); + }); + this.state.customSelectedStations.forEach(station => { + station_groups.push({ + stations: station.stations, + max_nr_missing: parseInt(station.max_nr_missing) + }); + }); + if(station_groups){ + station_groups.map(stationGroup =>{ + stationValue += stationGroup.stations+':'+stationGroup.max_nr_missing+"|"; + }) + } + await this.props.context.componentParent.updateDailyCell( + this.props.node.rowIndex,this.props.colDef.field, stationValue + ); + this.setState({ showDialog: false}); + +} + +onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customSelectedStations) => { + this.setState({ + ...state, + selectedStations, + missingStationFieldsErrors, + customSelectedStations + }, () => { + this.setState({ + validForm: this.validateForm() + }); + }); +}; + +render() { + return ( + <> + + <Dialog header={_.startCase(this.state.dialogTitle)} visible={this.state.showDialog} maximized={false} + onHide={() => {this.updateStationGroup()}} inputId="confirm_dialog" + footer={<div> + <Button key="back" label="Close" onClick={() => {this.updateStationGroup()}} /> + </div> + } > + <div className="ag-theme-balham" style={{ height: '90%', width: '1000px', paddingLeft: '20px' }}> + <Stations + stationGroup={this.state.stationGroup} + onUpdateStations={this.onUpdateStations.bind(this)} + /> + </div> + </Dialog> + + </> + ); +} + +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js index 685d11cc442e4f9bf1082706aad01a2fdc1c9186..ef773a00181db906bc02b311308c08e8cab813d0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/TimeInputmask.js @@ -2,6 +2,8 @@ import React, { Component } from 'react'; import { InputMask } from 'primereact/inputmask'; import Validator from '../../utils/validator'; +const BG_COLOR= '#f878788f'; + export default class TimeInputMask extends Component { constructor(props) { super(props); @@ -14,7 +16,7 @@ export default class TimeInputMask extends Component { e.originalEvent.target.style.backgroundColor = ''; isValid = true; }else{ - e.originalEvent.target.style.backgroundColor = 'orange'; + e.originalEvent.target.style.backgroundColor = BG_COLOR; } this.props.context.componentParent.updateAngle( diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/numericEditor.js b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/numericEditor.js index e759fd958dd2540ec4c2276793ac10443b586500..1662daa58c07829ad70ed7cd7c56416cbfb12124 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/numericEditor.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/Spreadsheet/numericEditor.js @@ -12,9 +12,7 @@ export default class NumericEditor extends Component { this.cancelBeforeStart = this.props.charPress && '1234567890'.indexOf(this.props.charPress) < 0; - this.state = this.createInitialState(props); - this.onKeyDown = this.onKeyDown.bind(this); this.handleChange = this.handleChange.bind(this); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/images/login-bg-1.jpg b/SAS/TMSS/frontend/tmss_webapp/src/images/login-bg-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2707a9905eb1b32168203380be4687f0fa764d8a Binary files /dev/null and b/SAS/TMSS/frontend/tmss_webapp/src/images/login-bg-1.jpg differ diff --git a/SAS/TMSS/frontend/tmss_webapp/src/images/login-bg-2.jpg b/SAS/TMSS/frontend/tmss_webapp/src/images/login-bg-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..405247dc8b9ec3a4be9f9e19250598e9efd929a3 Binary files /dev/null and b/SAS/TMSS/frontend/tmss_webapp/src/images/login-bg-2.jpg differ diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index 4ec1204d72a8ead8c5565e6457231059a9e82108..9c4949508bb5cec8f8366a008663a33e98c796b2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -22,6 +22,12 @@ border-top: none; } } +.ag-root-wrapper{ + /* + calendar is overlaped by AG-grid table, so table props update to show calendar + */ + overflow: inherit; +} .tmss-table { overflow:auto; // since calendar getting inserted to table, because of above overflow, not getting visible @@ -137,10 +143,40 @@ .plots{ padding-left: 2px; } - - - - - - +.list-stations-summary { + max-height: 200px; + overflow: auto; +} +.block-list { + > a { + display: block; + } +} +.se-resizing-bar .se-navigation.sun-editor-common { + display: none; +} +.se-component.se-image-container { + img { + width: 100%; + } +} +.p-button.tooltip-wrapper { + margin: 0; + padding: 0; + border: none; + background: none; + background-color: transparent; + box-shadow: none; + font-size: 1rem; + text-align: left; + color: #000; + .p-button-text.p-c{ + display: none; + } +} +.p-button.tooltip-wrapper:enabled:hover { + background-color: transparent; + color: #000; + border: none; +} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js index e45be034ce26e4db68ac6446a00f3517bc5f9184..f112943d779cdedc9448a0f7ff2f42ce10fab3c2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppTopbar.js @@ -4,24 +4,27 @@ import 'primeicons/primeicons.css'; import 'primereact/resources/themes/nova-light/theme.css'; import 'primereact/resources/primereact.css'; import 'primeflex/primeflex.css'; - import { PropTypes } from 'prop-types'; +import { PropTypes } from 'prop-types'; +import Auth from '../../authenticate/auth'; - export class AppTopbar extends Component { +export class AppTopbar extends Component { - // constructor(props) { - // super(props); - // } + constructor(props) { + super(props); + this.state = { + username: Auth.getUser().name + }; + } static defaultProps = { onToggleMenu: null - } - - - static propTypes = { - onToggleMenu: PropTypes.func.isRequired - } + } + + static propTypes = { + onToggleMenu: PropTypes.func.isRequired + } render() { return ( @@ -31,7 +34,15 @@ import 'primeflex/primeflex.css'; <button className="p-link layout-menu-button" onClick={this.props.onToggleMenu}> <i className="pi pi-bars"></i></button> <span className="header-title">TMSS</span> + {this.props.isLoggedIn && + <div className="top-right-bar"> + <span><i className="fa fa-user"></i>{this.state.username}</span> + <button className="p-link layout-menu-button" onClick={this.props.onLogout}> + <i className="pi pi-power-off"></i></button> + </div> + } </div> + </div> </React.Fragment> ) diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss index 302208b3dad39e6c4ebf092e614e502fbd131e37..1f4526c7df85ed761cf12441b914192264ac4fa3 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_layout.scss @@ -16,4 +16,5 @@ @import "./timeline"; @import "./_aggrid"; @import "./suSummary"; +@import "./login"; // @import "./splitpane"; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_login.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_login.scss new file mode 100644 index 0000000000000000000000000000000000000000..d1df063e62e02a4846902f89d81c5a9faf1de83b --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_login.scss @@ -0,0 +1,90 @@ +.login-page { + background-image: url(../../images/login-bg-1.jpg); + height: 100%; +} + +.bg-login{ + background-image: url(../../images/login-bg-1.jpg); + min-height: 100vh; + background-size: 100%; + padding: 20px; +} +.login-card{ + background-color: #fff; + float: none; + margin: auto; + box-shadow: 0 1px 15px rgba(0,0,0,.04), 0 1px 6px rgba(0,0,0,.04); + margin-top: 8%; + margin-bottom: 5%; +} +.detail-part{ + background-image: url(../../images/login-bg-2.jpg); + padding: 30px; + background-size: cover; + background-repeat: no-repeat; +} +.detail-part h3{ + color: #fff; + padding-top: 10%; + font-size: 2.5rem; +} +.detail-part p{ + color: #fff; + margin-top: 30px; + padding-left: 70%; +} +.logn-part{ + padding: 5%; +} +.logo-cover img{ + margin-bottom: 30px; +} +.form-cover h6{ + margin-bottom: 30px; +} +.form-cover input{ + margin-bottom: 30px; + border-radius: 0px; + background-color: #cccccc38; +} +.form-footer .forget-paswd{ + text-align: left; +} +.button-div{ + text-align: right; +} +.form-footer{ + margin-bottom: 50px; +} +.button-div .btn{ + background-color: #922c88 !important; + border-color: #922c88 !important; +} +.button-div .btn:hover{ + background-color: #922c88 !important; + border-color: #922c88 !important; +} +.button-div .btn:active{ + background-color: #922c88 !important; + border-color: #922c88 !important; +} +.button-div .btn:focus{ + background-color: #922c88 !important; + border-color: #922c88 !important; +} +.login-form .error { + text-transform: capitalize; +} +.login-form .form-field { + padding-top: 20px; + padding-bottom: 0px; +} +.login-form .form-field i { + padding-right: 5px; +} + +@media screen and (max-width: 1100px){ + .bg-login{ + background-image: url(../../images/login-bg-1.jpg); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss index e0c2e01575b7d261bb353d311a22730592c8af06..b4fb1605639537a250259c0662a3fe462e7a48c3 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_stations.scss @@ -82,3 +82,9 @@ padding: 0 !important; } } +/** +* Class to set margin-left for (i) and remove button in station.js +*/ +.icon-left{ + margin-left: 10px !important; +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_suSummary.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_suSummary.scss index 76b02736fb1959096ef3067f75b67f2ad1a72336..ab73c0560d155915f052c300f41c36280658e4ac 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_suSummary.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_suSummary.scss @@ -45,4 +45,11 @@ .json-to-table::-webkit-scrollbar-thumb { background-color: #0000007c; + } + + .station-list { + max-height: 150px; + overflow-y: scroll; + border: 1px solid lightgrey; + padding: 0px 10px 10px 10px; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss index e687ef7dae3dec800531b48174fbc34b7790b0dc..678f58f4053d806555173ff111674d31bd7cf47e 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_topbar.scss @@ -130,4 +130,17 @@ button { cursor: pointer; } +} + +.top-right-bar { + float: right; + padding-top: 5px +} + +.top-right-bar span>i { + padding-right: 5px; +} + +.top-right-bar button { + padding-left: 5px; } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js index 7a7a354a7fd5872a6ee6256abba1b92749a22538..ff68db5b7b0425c6954c70e9e73abb6847e6b241 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/Stations.js @@ -1,5 +1,3 @@ - - import React, { useState, useEffect } from 'react'; import _ from 'lodash'; import {MultiSelect} from 'primereact/multiselect'; @@ -225,9 +223,10 @@ export default (props) => { const updateSchedulingComp = (param_State, param_SelectedStations, param_missing_StationFieldsErrors, param_Custom_selected_options) => { const isError = param_missing_StationFieldsErrors.length || param_Custom_selected_options.filter(i => i.error).length; - debugger + // debugger props.onUpdateStations(param_State, param_SelectedStations, isError, param_Custom_selected_options); }; + /** * Method to remove the custom stations * @param {*} index number @@ -239,94 +238,118 @@ export default (props) => { updateSchedulingComp(state, selectedStations, missing_StationFieldsErrors, custom_selected_options); }; + const isPopup =() =>{ + return true; + } return ( - <div className="p-field p-grid grouping p-fluid"> + <div className={`p-field p-grid grouping p-fluid ${props.isSummary && 'p-col-12'}`}> <fieldset> <legend> <label>Stations<span style={{color:'red'}}>*</span></label> </legend> - {!props.view && <div className="col-sm-12 p-field p-grid" data-testid="stations"> - <div className="col-md-6 d-flex"> - <label htmlFor="stationgroup" className="col-sm-6 station_header">Station Groups</label> - <div className="col-sm-6"> - <MultiSelect data-testid="stations" id="stations" optionLabel="value" optionValue="value" filter={true} - tooltip="Select Stations" tooltipOptions={tooltipOptions} - value={selectedStations} - options={stationOptions} - placeholder="Select Stations" - onChange={(e) => setSelectedStationGroup(e.value)} - /> + {!props.isSummary && <> + {!props.view && <div className="col-sm-12 p-field p-grid" data-testid="stations"> + <div className="col-md-6 d-flex"> + <label htmlFor="stationgroup" className="col-sm-6 station_header">Station Groups</label> + <div className="col-sm-6"> + <MultiSelect data-testid="stations" id="stations" optionLabel="value" optionValue="value" filter={true} + tooltip="Select Stations" tooltipOptions={tooltipOptions} + value={selectedStations} + options={stationOptions} + placeholder="Select Stations" + onChange={(e) => setSelectedStationGroup(e.value)} + /> + </div> </div> - </div> - <div className="add-custom"> - <Button onClick={addCustom} label="Add Custom" icon="pi pi-plus" disabled={!stationOptions.length}/> - </div> - </div>} - {selectedStations.length ? <div className="col-sm-12 selected_stations" data-testid="selected_stations"> - {<div className="col-sm-12"><label style={{paddingLeft: '8px'}}>Maximum number of stations that can be missed in the selected groups</label></div>} - <div className="col-sm-12 p-0 d-flex flex-wrap"> - {selectedStations.map(i => ( - <div className="p-field p-grid col-md-6" key={i}> - <label className="col-sm-6 text-caps"> - {i} - <Button icon="pi pi-info-circle" className="p-button-rounded p-button-secondary p-button-text info" onClick={(e) => showStations(e, i)} /> - </label> - <div className="col-sm-6"> - <InputText id="missingstation" data-testid="name" - className={(state[i] && state[i].error) ?'input-error':''} - tooltip="Max No. of Missing Stations" tooltipOptions={tooltipOptions} maxLength="128" - placeholder="Max No. of Missing Stations" - value={state[i] ? state[i].missing_StationFields : ''} - disabled={props.view} - onChange={(e) => setNoOfmissing_StationFields(i, e.target.value)}/> - {(state[i] && state[i].error) && <span className="error-message">{state[i].missing_StationFields ? `Max. no of missing stations is ${state[i] ? state[i].stations.length : 0}` : 'Max. no of missing stations required'}</span>} - </div> - </div> - ))} - {customStations.map((stat, index) => ( - <div className="p-field p-grid col-md-12 custom-station-wrapper" key={index}> - {!props.view && <Button icon="pi pi-trash" className="p-button-secondary p-button-text custom-remove" onClick={() => removeCustomStations(index)} />} - - <div className="col-md-6 p-field p-grid"> - <label className="col-sm-6 text-caps custom-label"> - Custom {index + 1} + <div className="add-custom"> + <Button onClick={addCustom} label="Add Custom" icon="pi pi-plus" disabled={!stationOptions.length}/> + </div> + </div>} + {selectedStations.length ? <div className="col-sm-12 selected_stations" data-testid="selected_stations"> + {<div className="col-sm-12"><label style={{paddingLeft: '8px'}}>Maximum number of stations that can be missed in the selected groups</label></div>} + <div className="col-sm-12 p-0 d-flex flex-wrap"> + {selectedStations.map(i => ( + <div className="p-field p-grid col-md-6" key={i}> + <label className="col-sm-6 text-caps"> + {i} + <Button icon="pi pi-info-circle" className="p-button-rounded p-button-secondary p-button-text info" onClick={(e) => showStations(e, i)} /> </label> - <div className="col-sm-6 pr-8 custom-value"> - <MultiSelect data-testid="custom_stations" id="custom_stations" filter - tooltip="Select Stations" tooltipOptions={tooltipOptions} - value={stat.stations} - options={customStationsOptions} - placeholder="Select Stations" + <div className="col-sm-6"> + <InputText id="missingstation" data-testid="name" + className={(state[i] && state[i].error) ?'input-error':''} + tooltip="Max No. of Missing Stations" tooltipOptions={tooltipOptions} maxLength="128" + placeholder="Max No. of Missing Stations" + value={state[i] ? state[i].missing_StationFields : ''} disabled={props.view} - optionLabel="value" - optionValue="value" - onChange={(e) => onChangeCustomSelectedStations(e.value, index)} - /> + onChange={(e) => setNoOfmissing_StationFields(i, e.target.value)}/> + {(state[i] && state[i].error) && <span className="error-message">{state[i].missing_StationFields ? `Max. no of missing stations is ${state[i] ? state[i].stations.length : 0}` : 'Max. no of missing stations required'}</span>} </div> </div> - <div className="col-md-6 p-field p-grid"> - <label className="col-sm-6 customMissingStationLabel"> - Maximum No. Of Missing Stations - </label> - <div className="col-sm-6 pr-8 custom-field"> - <InputText id="missingStation" data-testid="name" - className={(stat.error && stat.touched) ?'input-error':''} - tooltip="Max Number of Missing Stations" tooltipOptions={tooltipOptions} - placeholder="Max Number of Missing Stations" - value={stat.max_nr_missing} - disabled={props.view} - onChange={(e) => setMissingFieldsForCustom(e.target.value, index)}/> - {(stat.error && stat.touched) && <span className="error-message">{stat.max_nr_missing ? `Max. no of missing stations is ${stat.stations.length}` : 'Max. no of missing stations required'}</span>} - {/* {props.view && - <span className="info">Max No. of Missing Stations</span>} */} - - </div> + ))} + {customStations.map((stat, index) => ( + <div className="p-field p-grid col-md-12 custom-station-wrapper" key={index}> + {!props.view && <Button icon="pi pi-trash" className="p-button-secondary p-button-text custom-remove" onClick={() => removeCustomStations(index)} />} + + <div className="col-md-6 p-field p-grid"> + <label className="col-sm-6 text-caps custom-label"> + Custom {index + 1} + </label> + <div className="col-sm-6 pr-8 custom-value"> + <MultiSelect data-testid="custom_stations" id="custom_stations" filter + tooltip="Select Stations" tooltipOptions={tooltipOptions} + value={stat.stations} + options={customStationsOptions} + placeholder="Select Stations" + disabled={props.view} + optionLabel="value" + optionValue="value" + onChange={(e) => onChangeCustomSelectedStations(e.value, index)} + /> + </div> + </div> + <div className="col-md-6 p-field p-grid"> + <label className="col-sm-6 customMissingStationLabel"> + Maximum No. Of Missing Stations + </label> + <div className="col-sm-6 pr-8 custom-field"> + <InputText id="missingStation" data-testid="name" + className={(stat.error && stat.touched) ?'input-error':''} + tooltip="Max Number of Missing Stations" tooltipOptions={tooltipOptions} + placeholder="Max Number of Missing Stations" + value={stat.max_nr_missing} + disabled={props.view} + onChange={(e) => setMissingFieldsForCustom(e.target.value, index)}/> + {(stat.error && stat.touched) && <span className="error-message">{stat.max_nr_missing ? `Max. no of missing stations is ${stat.stations.length}` : 'Max. no of missing stations required'}</span>} + {/* {props.view && + <span className="info">Max No. of Missing Stations</span>} */} + + </div> + </div> </div> - </div> - ))} + ))} + </div> + + </div> : null} + </>} + {/* For timeline view, displaying all stations in list */} + {props.isSummary && ( + <div className="list-stations-summary"> + {state && Object.keys(state).map(key => { + if (key !== 'Custom') { + return ( + <> + {state[key].stations.map((station, index) => <div key={index}>{station}</div>)} + </> + ) + } + })} + {customStations.map((stat, index) => ( + <> + {stat.stations.map(station => <div key={index}>{station}</div>)} + </> + ))} </div> - - </div> : null} + )} <OverlayPanel ref={(el) => op = el} dismissable style={{width: '450px'}}> <div className="station-container"> {(stations || []).map(i => ( diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js index 2e330e0fbc70c7708dd629f2a17e1229e8507b18..227e677f8a39dec732b95bd1fb45010d42b7fef2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.scheduleset.js @@ -1,28 +1,45 @@ import React, {Component} from 'react'; -import { Link, Redirect } from 'react-router-dom'; -import _ from 'lodash'; +import { Redirect } from 'react-router-dom'; import {Dropdown} from 'primereact/dropdown'; import { Button } from 'primereact/button'; import {Dialog} from 'primereact/components/dialog/Dialog'; import {Growl} from 'primereact/components/growl/Growl'; +import { AgGridReact } from 'ag-grid-react'; +import { AllCommunityModules } from '@ag-grid-community/all-modules'; +import $RefParser from "@apidevtools/json-schema-ref-parser"; + +import TimeInputmask from './../../components/Spreadsheet/TimeInputmask' +import DegreeInputmask from './../../components/Spreadsheet/DegreeInputmask' +import NumericEditor from '../../components/Spreadsheet/numericEditor'; +import BetweenEditor from '../../components/Spreadsheet/BetweenEditor'; +import BetweenRenderer from '../../components/Spreadsheet/BetweenRenderer'; +import MultiSelector from '../../components/Spreadsheet/MultiSelector'; import AppLoader from '../../layout/components/AppLoader'; + +import PageHeader from '../../layout/components/PageHeader'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import ProjectService from '../../services/project.service'; import ScheduleService from '../../services/schedule.service'; import TaskService from '../../services/task.service'; +import CustomDateComp from '../../components/Spreadsheet/CustomDateComp'; + +import Validator from '../../utils/validator'; +import UnitConverter from '../../utils/unit.converter' import UIConstants from '../../utils/ui.constants'; -import $RefParser from "@apidevtools/json-schema-ref-parser"; -import TimeInputmask from './../../components/Spreadsheet/TimeInputmask' -import DegreeInputmask from './../../components/Spreadsheet/DegreeInputmask' -import NumericEditor from '../../components/Spreadsheet/numericEditor'; +import UnitConversion from '../../utils/unit.converter'; +import StationEditor from '../../components/Spreadsheet/StationEditor'; + + +import moment from 'moment'; +import _ from 'lodash'; -import { AgGridReact } from 'ag-grid-react'; -import { AllCommunityModules } from '@ag-grid-community/all-modules'; import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-theme-alpine.css'; -import UnitConverter from '../../utils/unit.converter' -import Validator from '../../utils/validator'; -import PageHeader from '../../layout/components/PageHeader'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +const BG_COLOR= '#f878788f'; + /** * Component to create / update Scheduling Unit Drafts using Spreadsheet */ @@ -33,10 +50,14 @@ export class SchedulingSetCreate extends Component { this.gridColumnApi = '' this.rowData = []; this.tmpRowData = []; - + this.defaultCellValues = []; + this.daily = []; + this.state = { + dailyOption: [], projectDisabled: (props.match?(props.match.params.project? true:false):false), - isLoading: true, // Flag for loading spinner + isLoading: true, + isAGLoading: false, // Flag for loading spinner dialog: { header: '', detail: ''}, // Dialog properties redirect: null, // URL to redirect errors: [], // Form Validation errors @@ -61,6 +82,11 @@ export class SchedulingSetCreate extends Component { numericEditor: NumericEditor, timeInputMask: TimeInputmask, degreeInputMask: DegreeInputmask, + betweenRenderer: BetweenRenderer, + betweenEditor: BetweenEditor, + multiselector: MultiSelector, + agDateInput: CustomDateComp, + station: StationEditor, }, columnTypes: { numberValueColumn: { @@ -79,6 +105,8 @@ export class SchedulingSetCreate extends Component { rowIdRenderer: function (params) { return 1 + params.rowIndex; }, + validCount: 0, + inValidCount: 0, }, noOfSUOptions: [ { label: '10', value: '10' }, @@ -87,6 +115,10 @@ export class SchedulingSetCreate extends Component { { label: '250', value: '250' }, { label: '500', value: '500' } ], + customSelectedStations: [], + selectedStations: [], + defaultStationGroups: [], + saveDialogVisible: false, } this.onGridReady = this.onGridReady.bind(this); @@ -96,7 +128,11 @@ export class SchedulingSetCreate extends Component { this.cancelCreate = this.cancelCreate.bind(this); this.clipboardEvent = this.clipboardEvent.bind(this); this.reset = this.reset.bind(this); - + this.close = this.close.bind(this); + this.saveSU = this.saveSU.bind(this); + this.validateGridAndSave = this.validateGridAndSave.bind(this); + this.showDialogContent = this.showDialogContent.bind(this); + this.projects = []; // All projects to load project dropdown this.schedulingSets = []; // All scheduling sets to be filtered for project this.observStrategies = []; // All Observing strategy templates @@ -108,7 +144,7 @@ export class SchedulingSetCreate extends Component { scheduling_set_id: {required: true, message: "Select the Scheduling Set"}, }; } - + componentDidMount() { const promises = [ ProjectService.getProjectList(), ScheduleService.getSchedulingSets(), @@ -136,7 +172,7 @@ export class SchedulingSetCreate extends Component { const projectSchedluingSets = _.filter(this.schedulingSets, {'project_id': projectName}); let schedulingUnit = this.state.schedulingUnit; schedulingUnit.project = projectName; - this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedluingSets, validForm: this.validateForm('project')}); + this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedluingSets, validForm: this.validateForm('project'), rowData: [],observStrategy: {}}); } /** @@ -144,57 +180,80 @@ export class SchedulingSetCreate extends Component { * @param {string} key * @param {object} value */ - async setSchedingSetParams(key, value) { + async setSchedulingSetParams(key, value) { + this.setState({isAGLoading: true}); + let schedulingUnit = this.state.schedulingUnit; schedulingUnit[key] = value; let schedulingUnitList = await ScheduleService.getSchedulingBySet(value); - if(schedulingUnitList){ + if (schedulingUnitList) { const schedulingSetIds = _.uniq(_.map(schedulingUnitList, 'observation_strategy_template_id')); - if(schedulingSetIds.length === 1){ + if (schedulingSetIds.length === 1) { const observStrategy = _.find(this.observStrategies, {'id': schedulingUnitList[0].observation_strategy_template_id}); + this.setDefaultStationGroup(observStrategy); this.setState({ schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), - schedulingUnitList: schedulingUnitList, schedulingSetId: value, selectedSchedulingSetId: value, observStrategy: observStrategy + schedulingUnitList: schedulingUnitList, schedulingSetId: value, selectedSchedulingSetId: value, observStrategy: observStrategy, }); await this.prepareScheduleUnitListForGrid(); - }else{ + } else { /* Let user to select Observation Strategy */ this.setState({ rowData:[], schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), schedulingUnitList:schedulingUnitList, selectedSchedulingSetId: value, observStrategy: {} }); } - }else{ + } else { this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), selectedSchedulingSetId: value}); } + this.setState({isAGLoading: false}); } + async setDefaultStationGroup(observStrategy) { + let station_group = []; + const tasks = observStrategy.template.tasks; + for (const taskName of _.keys(tasks)) { + const task = tasks[taskName]; + //Resolve task from the strategy template + const $taskRefs = await $RefParser.resolve(task); + // Identify the task specification template of every task in the strategy template + const taskTemplate = _.find(this.taskTemplates, {'name': task['specifications_template']}); + if (taskTemplate.type_value==='observation' && task.specifications_doc.station_groups) { + station_group = task.specifications_doc.station_groups; + } + } + await this.setState({ + defaultStationGroups: station_group, + }) + } /** * Function called when observation strategy template is changed. * * @param {number} strategyId */ async changeStrategy (strategyId) { + this.setState({isAGLoading: true}); const observStrategy = _.find(this.observStrategies, {'id': strategyId}); let schedulingUnitList= await ScheduleService.getSchedulingBySet(this.state.selectedSchedulingSetId); schedulingUnitList = _.filter(schedulingUnitList,{'observation_strategy_template_id': strategyId}) ; - + this.setDefaultStationGroup(observStrategy); await this.setState({ schedulingUnitList: schedulingUnitList, - observStrategy: observStrategy + observStrategy: observStrategy, }) - if(schedulingUnitList && schedulingUnitList.length >0){ + if (schedulingUnitList && schedulingUnitList.length >0){ await this.prepareScheduleUnitListForGrid(); - }else{ + } else { this.setState({ rowData: [] }) } - this.state.gridApi.setRowData(this.state.rowData) - this.state.gridApi.redrawRows(); + // this.state.gridApi.setRowData(this.state.rowData) + //this.state.gridApi.redrawRows(); + this.setState({isAGLoading: false}); } /** @@ -212,10 +271,10 @@ export class SchedulingSetCreate extends Component { if (refUrl.endsWith("/pointing")) { // For type pointing schema.definitions["pointing"] = (await $RefParser.resolve(refUrl)).get(newRef); property["$ref"] = newRef; - } else { // General object to resolve if any reference in child level + } else { // General object to resolve if any reference in child level property = await this.resolveSchema((await $RefParser.resolve(refUrl)).get(newRef)); } - } else if(property["type"] === "array") { // reference in array items definition + } else if (property["type"] === "array") { // reference in array items definition let resolvedItems = await this.resolveSchema(property["items"]); schema.definitions = {...schema.definitions, ...resolvedItems.definitions}; delete resolvedItems['definitions']; @@ -243,6 +302,10 @@ export class SchedulingSetCreate extends Component { return schema; } + async getConstraintSchema(scheduleUnit){ + let constraintSchema = await ScheduleService.getSchedulingConstraintTemplate(scheduleUnit.scheduling_constraints_template_id); + return constraintSchema; + } /** * Function to generate AG-Grid column definition. @@ -251,24 +314,57 @@ export class SchedulingSetCreate extends Component { async createGridColumns(scheduleUnit){ let schema = await this.getTaskSchema(scheduleUnit); schema = await this.resolveSchema(schema); + let constraintSchema = await this.getConstraintSchema(scheduleUnit); + constraintSchema = await this.resolveSchema(constraintSchema); + // AG Grid Cell Specific Properties - const cellProps =[]; - cellProps['angle1'] = {type:'numberValueColumn', cellRenderer: 'timeInputMask',cellEditor: 'timeInputMask', valueSetter: 'valueSetter' }; - cellProps['angle2'] = {type:'numberValueColumn', cellRenderer: 'degreeInputMask',cellEditor: 'degreeInputMask', valueSetter: 'valueSetter' }; - cellProps['angle3'] = {cellEditor: 'numericEditor',}; - cellProps['direction_type'] = {cellEditor: 'agSelectCellEditor',default: schema.definitions.pointing.properties.direction_type.default, - cellEditorParams: { - values: schema.definitions.pointing.properties.direction_type.enum, - }, - }; - //Ag-grid Colums definition + let dailyOption= []; + let dailyProps = Object.keys( constraintSchema.schema.properties.daily.properties); + this.daily = []; + dailyProps.forEach(prop => { + dailyOption.push({'Name':prop, 'Code':prop}); + this.daily.push(prop); + }) + + this.setState({ + dailyOption: this.dailyOption, + schedulingConstraintsDoc: scheduleUnit.scheduling_constraints_doc, + constraintUrl: scheduleUnit.scheduling_constraints_template, + constraintId: scheduleUnit.scheduling_constraints_template_id, + daily: this.daily, + }); + let cellProps =[]; + cellProps['angle1'] = {isgroup: true, type:'numberValueColumn', cellRenderer: 'timeInputMask',cellEditor: 'timeInputMask', valueSetter: 'valueSetter', }; + cellProps['angle2'] = {isgroup: true, type:'numberValueColumn', cellRenderer: 'degreeInputMask',cellEditor: 'degreeInputMask', valueSetter: 'valueSetter' }; + cellProps['angle3'] = {isgroup: true, cellEditor: 'numericEditor', cellStyle: function(params) { if (params.value){ + if (!Number(params.value)) { + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < 0|| Number(params.value) > 90) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + }}}; + cellProps['direction_type'] = {isgroup: true, cellEditor: 'agSelectCellEditor',default: schema.definitions.pointing.properties.direction_type.default, + cellEditorParams: { + values: schema.definitions.pointing.properties.direction_type.enum, + }, + }; + + //Ag-grid Colums definition + // Column order to use clipboard copy let colKeyOrder = []; + + colKeyOrder.push("suname"); + colKeyOrder.push("sudesc"); + let columnMap = []; let colProperty = {}; let columnDefs = [ { // Row Index - headerName: '', + headerName: '#', editable: false, maxWidth: 60, cellRenderer: 'rowIdRenderer', @@ -280,16 +376,155 @@ export class SchedulingSetCreate extends Component { headerName: 'Scheduling Unit', children: [ {headerName: 'Name',field: 'suname'}, - {headerName: 'Description',field: 'sudesc'} + {headerName: 'Description',field: 'sudesc', cellStyle: function(params) { + if (params.data.suname && params.data.suname !== '' && params.value === '') { + return { backgroundColor: BG_COLOR}; + } else { return { backgroundColor: ''};} + }, + } ], - } + }, + + { headerName: 'Scheduler',field: 'scheduler',cellEditor: 'agSelectCellEditor',default: constraintSchema.schema.properties.scheduler.default, + cellEditorParams: { + values: constraintSchema.schema.properties.scheduler.enum, + }, + }, + { headerName: 'Time', + children: [ + { headerName: 'At', field:'timeat', editable: true, cellRenderer: 'betweenRenderer',cellEditor: 'agDateInput', valueSetter: 'newValueSetter'}, + { headerName: 'After', field:'timeafter', editable: true, cellRenderer: 'betweenRenderer',cellEditor: 'agDateInput', valueSetter: 'newValueSetter'}, + { headerName: 'Before', field:'timebefore', editable: true, cellRenderer: 'betweenRenderer',cellEditor: 'agDateInput', valueSetter: 'newValueSetter'}, + ], + }, + + {headerName: 'Between',field: 'between',cellRenderer: 'betweenRenderer',cellEditor: 'betweenEditor',valueSetter: 'newValueSetter', }, + {headerName: 'Not Between',field: 'notbetween',cellRenderer: 'betweenRenderer',cellEditor: 'betweenEditor',valueSetter: 'newValueSetter'}, + {headerName: 'Daily',field: 'daily',cellEditor: 'multiselector', valueSetter: 'valueSetter'}, + { + headerName: 'Sky', + children: [ + {headerName: 'Min Target Elevation',field: 'min_target_elevation', cellStyle: function(params) { + if (params.value){ + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < 0|| Number(params.value) > 90) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } + }, }, + {headerName: 'Min Calibrator Elevation',field: 'min_calibrator_elevation', cellStyle: function(params) { + if (params.value){ + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < 0|| Number(params.value) > 90) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } + }, }, + {headerName: 'Offset Window From',field: 'offset_from', cellStyle: function(params) { + if (params.value){ + if (params.value === 'undefined' || params.value === ''){ + return { backgroundColor: ''}; + } + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < -0.20943951 || Number(params.value) > 0.20943951) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } else { + return { backgroundColor: ''}; + } + }, }, + {headerName: 'Offset Window To',field: 'offset_to', cellStyle: function(params) { + if (params.value){ + if (params.value === 'undefined' || params.value === ''){ + return { backgroundColor: ''}; + } + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < -0.20943951 || Number(params.value) > 0.20943951) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } else { + return { backgroundColor: ''}; + } + }, }, + ], + }, + { + headerName: 'Min_distance', + children: [ + {headerName: 'Sun',field: 'md_sun', cellStyle: function(params) { + if (params.value){ + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < 0 || Number(params.value) > 180) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } + + },}, + {headerName: 'Moon',field: 'md_moon', cellStyle: function(params) { + if (params.value){ + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < 0 || Number(params.value) > 180) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } + }, }, + {headerName: 'Jupiter',field: 'md_jupiter', cellStyle: function(params) { + if (params.value){ + if ( !Number(params.value)){ + return { backgroundColor: BG_COLOR}; + } + else if ( Number(params.value) < 0 || Number(params.value) > 180) { + return { backgroundColor: BG_COLOR}; + } else{ + return { backgroundColor: ''}; + } + } + }, }, + ], + }, ]; - colKeyOrder.push("suname"); - colKeyOrder.push("sudesc"); + colKeyOrder.push('scheduler'); + colKeyOrder.push('timeat'); + colKeyOrder.push('timeafter'); + colKeyOrder.push('timebefore'); + colKeyOrder.push('between'); + colKeyOrder.push('notbetween'); + colKeyOrder.push('daily'); + colKeyOrder.push('min_target_elevation'); + colKeyOrder.push('min_calibrator_elevation'); + colKeyOrder.push('offset_from'); + colKeyOrder.push('offset_to'); + colKeyOrder.push('md_sun'); + colKeyOrder.push('md_moon'); + colKeyOrder.push('md_jupiter'); colProperty ={'ID':'id', 'Name':'suname', 'Description':'sudesc'}; columnMap['Scheduling Unit'] = colProperty; - + let definitions = schema.definitions.pointing.properties; let properties = schema.properties; const propsKeys = Object.keys(properties); @@ -319,17 +554,16 @@ export class SchedulingSetCreate extends Component { }) columnMap[property.title] = colProperty; } - colProperty ={'From':'bfrom', 'Until':'buntil'}; - columnMap['Between'] = colProperty; + columnDefs.push({headerName: 'Stations', field: 'stations', cellRenderer: 'betweenRenderer', cellEditor: 'station', valueSetter: 'newValueSetter'}); + colKeyOrder.push('stations'); this.setState({ columnDefs:columnDefs, columnMap:columnMap, colKeyOrder:colKeyOrder }) - } - - async getTaskSchema(scheduleUnit){ + + async getTaskSchema(scheduleUnit) { let strategyId = scheduleUnit.observation_strategy_template_id; let tasksToUpdate = {}; const observStrategy = _.find(this.observStrategies, {'id': strategyId}); @@ -391,18 +625,83 @@ export class SchedulingSetCreate extends Component { } return schema; } + /** + * CallBack Function : update time value in master grid + */ + async updateTime(rowIndex, field, value) { + let row = this.state.rowData[rowIndex]; + row[field] = value; + let tmpRowData =this.state.rowData; + tmpRowData[rowIndex]= row; + await this.setState({ + rowData: tmpRowData + }); + this.state.gridApi.setRowData(this.state.rowData); + this.state.gridApi.redrawRows(); + } + /** + * Update the Daily column value from external component + * @param {*} rowIndex + * @param {*} field + * @param {*} value + */ + async updateDailyCell(rowIndex, field, value) { + let row = this.state.rowData[rowIndex]; + row[field] = value; + let tmpRowData =this.state.rowData; + tmpRowData[rowIndex]= row; + await this.setState({ + rowData: tmpRowData + }); + } + + async getStationGrops(schedulingUnit){ + let stationValue = ''; + if (schedulingUnit && schedulingUnit.id>0) { + const promises = await [ + ScheduleService.getObservationStrategies(), + TaskService.getTaskTemplates(), + ScheduleService.getSchedulingUnitDraftById(schedulingUnit.id), + ScheduleService.getTasksDraftBySchedulingUnitId(schedulingUnit.id), + ScheduleService.getStationGroup() + ]; + await Promise.all(promises).then(responses => { + this.observStrategies = responses[0]; + this.taskTemplates = responses[1]; + let schedulingUnit = responses[2]; + let taskDrafts = responses[3]; + this.stations = responses[4]; + let stationGroups = []; + if (schedulingUnit && schedulingUnit.observation_strategy_template_id) { + let targetObservation = schedulingUnit.requirements_doc.tasks['Target Observation']; + if (targetObservation && targetObservation.specifications_doc.station_groups){ + stationGroups = targetObservation?targetObservation.specifications_doc.station_groups:[]; + } else { + targetObservation = taskDrafts.data.results.find(task => {return task.specifications_doc.station_groups?true:false}); + stationGroups = targetObservation?targetObservation.specifications_doc.station_groups:[]; + } + } + + if (stationGroups) { + stationGroups.map(stationGroup =>{ + stationValue += stationGroup.stations+':'+stationGroup.max_nr_missing+"|"; + }) + } + }); + } + return stationValue; + } /** * Function to prepare ag-grid row data. */ async prepareScheduleUnitListForGrid(){ - if(this.state.schedulingUnitList.length===0){ + if (this.state.schedulingUnitList.length===0) { return; } this.tmpRowData = []; let totalSU = this.state.noOfSU; - let paramsOutput = {}; //refresh column header await this.createGridColumns(this.state.schedulingUnitList[0]); let observationPropsList = []; @@ -417,21 +716,53 @@ export class SchedulingSetCreate extends Component { let parameters = scheduleunit['requirements_doc'].parameters; for(const parameter of parameters){ - let rurl = parameter['refs']; - let valueItem = (await $RefParser.resolve( scheduleunit['requirements_doc'])).get(rurl[0]); + let refUrl = parameter['refs']; + let valueItem = (await $RefParser.resolve( scheduleunit['requirements_doc'])).get(refUrl[0]); let excelColumns = this.state.columnMap[parameter.name]; let excelColumnsKeys = Object.keys(excelColumns); for(const eColKey of excelColumnsKeys){ - if(eColKey === 'angle1'){ + if (eColKey === 'angle1') { observationProps[excelColumns[eColKey]] = UnitConverter.getAngleInput(valueItem[eColKey], false); } - else if(eColKey === 'angle2'){ + else if (eColKey === 'angle2') { observationProps[excelColumns[eColKey]] = UnitConverter.getAngleInput(valueItem[eColKey], true); } - else{ + else { observationProps[excelColumns[eColKey]] = valueItem[eColKey]; } + } + } + observationProps['stations'] = await this.getStationGrops(scheduleunit); + let constraint = scheduleunit.scheduling_constraints_doc; + if (constraint){ + if (constraint.scheduler){ + observationProps['scheduler'] = constraint.scheduler; + } + observationProps['timeat'] = moment.utc(constraint.time.at).format(DATE_TIME_FORMAT); + observationProps['timeafter'] = moment.utc(constraint.time.after).format(DATE_TIME_FORMAT); + observationProps['timebefore'] = moment.utc(constraint.time.before).format(DATE_TIME_FORMAT); + if (constraint.time.between){ + observationProps['between'] = this.getBetweenStringValue(constraint.time.between); + } + if (constraint.time.between){ + observationProps['notbetween'] = this.getBetweenStringValue(constraint.time.not_between); + } + + observationProps['daily'] = this.fetchDailyFieldValue(constraint.daily); + UnitConversion.radiansToDegree(constraint.sky); + observationProps['min_target_elevation'] = constraint.sky.min_target_elevation; + observationProps['min_calibrator_elevation'] = constraint.sky.min_calibrator_elevation; + if ( constraint.sky.transit_offset ){ + observationProps['offset_from'] = (constraint.sky.transit_offset.from)?constraint.sky.transit_offset.from:''; + observationProps['offset_to'] = (constraint.sky.transit_offset.to)?constraint.sky.transit_offset.to:''; + } + + if (constraint.sky.min_distance){ + observationProps['md_sun'] = (constraint.sky.min_distance.sun)?constraint.sky.min_distance.sun:''; + observationProps['md_moon'] = (constraint.sky.min_distance.moon)?constraint.sky.min_distance.moon:''; + observationProps['md_jupiter'] = (constraint.sky.min_distance.jupiter)?constraint.sky.min_distance.jupiter:''; } + } observationPropsList.push(observationProps); } @@ -440,18 +771,18 @@ export class SchedulingSetCreate extends Component { // find No. of rows filled in array let totalCount = this.tmpRowData.length; // Prepare No. Of SU for rows for UI - if(this.tmpRowData && this.tmpRowData.length>0){ + if (this.tmpRowData && this.tmpRowData.length>0){ const paramsOutputKey = Object.keys( this.tmpRowData[0]); const availableCount = this.tmpRowData.length; - if(availableCount>= totalSU){ - totalSU = availableCount+10; + if (availableCount >= totalSU){ + totalSU = availableCount+5; } for(var i = availableCount; i<totalSU; i++){ let emptyRow = {}; paramsOutputKey.forEach(key =>{ - if(key === 'id'){ + if (key === 'id'){ emptyRow[key]= 0; - }else{ + } else { emptyRow[key]= ''; } }) @@ -466,6 +797,24 @@ export class SchedulingSetCreate extends Component { }); } + /** + * Get Daily column value + * @param {*} daily + */ + fetchDailyFieldValue(daily){ + let returnValue = []; + if (daily.require_day === true){ + returnValue.push('require_day'); + } + if (daily.require_night === true){ + returnValue.push('require_night'); + } + if (daily.avoid_twilight === true){ + returnValue.push('avoid_twilight'); + } + return returnValue; + } + /** * Function called back from Degree/Time Input Mask to set value in row data. * @@ -482,7 +831,6 @@ export class SchedulingSetCreate extends Component { await this.setState({ rowData: tmpRowData }); - } /** @@ -491,7 +839,7 @@ export class SchedulingSetCreate extends Component { async readClipBoard(){ try{ const queryOpts = { name: 'clipboard-read', allowWithoutGesture: true }; - const permissionStatus = await navigator.permissions.query(queryOpts); + await navigator.permissions.query(queryOpts); let data = await navigator.clipboard.readText(); return data; }catch(err){ @@ -499,28 +847,32 @@ export class SchedulingSetCreate extends Component { } } - /** - * Check the content is JSON format - * @param {*} jsonData - */ - async isJsonData(jsonData){ - try{ - let jsonObj = JSON.parse(jsonData); - return true; - }catch(err){ - console.log("error :",err) - return false; + /* + // to resolve the invalid degree and time + resolveCellData(data){ + console.log('data >',data) + let angleData = _.split(data, ":"); + let returnValue =''; + if (angleData.length === 3){ + returnValue = (angleData[0].length === 2)?angleData[0] :'0'+angleData[0]+":"; + returnValue += (angleData[1].length === 2)?angleData[1] :'0'+angleData[1]+":"; + returnValue += (angleData[2].length === 2)?angleData[2] :'0'+angleData[2]; + } - } + console.log('returnValue',returnValue) + return returnValue; + } + */ /** * Copy data to/from clipboard * @param {*} e */ async clipboardEvent(e){ - var key = e.which || e.keyCode; // keyCode detection - var ctrl = e.ctrlKey ? e.ctrlKey : ((key === 17) ? true : false); // ctrl detection - if ( key == 86 && ctrl ) { + //let angleCellKey = ['tp1angle1','tp1angle2','tp2angle1','tp2angle2','tpangle1','tpangle2']; + var key = e.which || e.keyCode; + var ctrl = e.ctrlKey ? e.ctrlKey : ((key === 17) ? true : false); + if ( key === 86 && ctrl ) { // Ctrl+V this.tmpRowData = this.state.rowData; let dataRowCount = this.state.totalCount; @@ -532,7 +884,8 @@ export class SchedulingSetCreate extends Component { }catch(err){ console.log("error :",err); } - if(clipboardData){ + if (clipboardData){ + clipboardData = _.trim(clipboardData); let suGridRowData= this.state.emptyRow; clipboardData = _.trim(clipboardData); let suRows = clipboardData.split("\n"); @@ -543,7 +896,11 @@ export class SchedulingSetCreate extends Component { suGridRowData['id']= 0; suGridRowData['isValid']= true; for(const key of this.state.colKeyOrder){ - suGridRowData[key]= suRow[colCount]; + /* if (_.includes(angleCellKey, key)){ + suGridRowData[key]= this.resolveCellData(suRow[colCount]); + } else {*/ + suGridRowData[key]= suRow[colCount]; + // } colCount++; } this.tmpRowData[dataRowCount]= (suGridRowData); @@ -552,8 +909,8 @@ export class SchedulingSetCreate extends Component { } let emptyRow = this.state.emptyRow; let tmpNoOfSU= this.state.noOfSU; - if(dataRowCount >= tmpNoOfSU){ - tmpNoOfSU = dataRowCount+10; + if (dataRowCount >= tmpNoOfSU){ + tmpNoOfSU = dataRowCount+5; //Create additional empty row at the end for(let i= this.tmpRowData.length; i<=tmpNoOfSU; i++){ this.tmpRowData.push(emptyRow); @@ -573,7 +930,7 @@ export class SchedulingSetCreate extends Component { console.error('Error: ', err); } - } else if ( key == 67 && ctrl ) { + } else if ( key === 67 && ctrl ) { //Ctrl+C var selectedRows = this.state.gridApi.getSelectedRows(); let clipboardData = ''; @@ -586,28 +943,168 @@ export class SchedulingSetCreate extends Component { clipboardData += line + '\r\n'; } clipboardData = _.trim(clipboardData); + const queryOpts = { name: 'clipboard-write', allowWithoutGesture: true }; await navigator.permissions.query(queryOpts); await navigator.clipboard.writeText(clipboardData); + } else if ( key === 46){ + // Delete selected rows + let tmpRowData = this.state.rowData; + + var selectedRows = this.state.gridApi.getSelectedNodes(); + if (selectedRows){ + await selectedRows.map(delRow =>{ + delete tmpRowData[delRow.rowIndex] + }); + await this.setState({ + rowData: tmpRowData + }); + this.state.gridApi.setRowData(this.state.rowData); + this.state.gridApi.redrawRows(); + } } } + + /** + * Validate Grid values on click Save button from UI + */ + async validateGridAndSave(){ + let validCount = 0; + let inValidCount = 0; + let isValidRow = true; + let errorDisplay = []; + const mandatoryKeys = ['suname','sudesc','scheduler','min_target_elevation','min_calibrator_elevation','offset_from','offset_to','md_sun','md_moon','md_jupiter','tp1angle1','tp1angle2','tp1angle3','tp1direction_type','tp2angle1','tp2angle2','tp2angle3','tp2direction_type','tbangle1','tbangle2','tbangle3','tbdirection_type']; + let tmpMandatoryKeys = []; + let tmpRowData = this.state.rowData; + this.state.gridApi.forEachNode(function (node) { + isValidRow = true; + let errorMsg = 'Row Id ['+(Number(node.rowIndex)+1) +'] : '; + tmpMandatoryKeys = []; + const rowData = node.data; + let isManualScheduler = false; + if (rowData) { + for(const key of mandatoryKeys) { + if (rowData[key] === '') { + tmpMandatoryKeys.push(key); + } else if (key === 'scheduler' && rowData[key] === 'manual' ) { + isManualScheduler = true; + } + } + if (tmpMandatoryKeys.length !== mandatoryKeys.length) { + let rowNoColumn = {}; + isValidRow = true; + for (var i = 0; i< node.columnController.gridColumns.length; i++) { + let column = node.columnController.gridColumns[i]; + if (column.colId === '0'){ + rowNoColumn = column; + } else { + if (_.includes(tmpMandatoryKeys, column.colId)){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + //column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + //rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } else { + if (column.colId === 'timeat' && isManualScheduler && rowData[column.colId] === ''){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + // column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } else if (column.colId === 'min_target_elevation' || column.colId === 'min_calibrator_elevation' || _.endsWith(column.colId, "angle3")){ + if (Number(rowData[column.colId]) < 0 || Number(rowData[column.colId]) > 90){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + // column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } + } else if (column.colId === 'offset_from' || column.colId === 'offset_to'){ + if ( !Number(rowData[column.colId])){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + // column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } else if ( Number(rowData[column.colId]) < -0.20943951 || Number(rowData[column.colId]) > 0.20943951) { + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + //column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } + } else if (column.colId === 'md_sun' || column.colId === 'md_moon' || column.colId === 'md_jupiter'){ + if (Number(rowData[column.colId]) < 0 || Number(rowData[column.colId]) > 180){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + // column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } + } else if (_.endsWith(column.colId, "angle1") && !Validator.validateTime(rowData[column.colId])){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + //column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + // rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } else if (_.endsWith(column.colId, "angle2") && !Validator.validateAngle(rowData[column.colId])){ + isValidRow = false; + errorMsg += column.colDef.headerName+", "; + //column.colDef.cellStyle = { backgroundColor: BG_COLOR}; + //rowNoColumn.colDef.cellStyle = { backgroundColor: BG_COLOR}; + } + } + } + } + } + } + if (isValidRow) { + validCount++; + tmpRowData[node.rowIndex]['isValid'] = true; + } else { + inValidCount++; + tmpRowData[node.rowIndex]['isValid'] = false; + errorDisplay.push(errorMsg.slice(0, -2)); + } + }); + + if (validCount > 0 && inValidCount === 0) { + // save SU directly + this. saveSU(); + } else if (validCount === 0 && inValidCount === 0) { + // leave with no change + } else { + this.setState({ + validCount: validCount, + inValidCount: inValidCount, + tmpRowData: tmpRowData, + saveDialogVisible: true, + errorDisplay: errorDisplay, + }); + this.state.gridApi.redrawRows(); + } + } /** * Function to create Scheduling unit */ - async saveSchedulingUnit() { + async saveSchedulingUnit(){ + this.validateGridAndSave(); + } + + + /** + * Save/Update Scheduling Unit + */ + async saveSU() { let newSUCount = 0; let existingSUCount = 0; try{ + this.setState({ + saveDialogVisible: false + }) let observStrategy = _.cloneDeep(this.state.observStrategy); const $refs = await $RefParser.resolve(observStrategy.template); let newSU = this.state.schedulingUnit; let parameters = this.state.schedulingUnitList[0]['requirements_doc'].parameters; let columnMap = this.state.columnMap; - + for(const suRow of this.state.rowData){ - if(!suRow['isValid']){ + if (!suRow['isValid']){ continue; } let validRow = true; @@ -618,59 +1115,199 @@ export class SchedulingSetCreate extends Component { let result = columnMap[parameter.name]; let resultKeys = Object.keys(result); resultKeys.forEach(key =>{ - if(key === 'angle1'){ - if(!Validator.validateTime(suRow[result[key]])){ + if (key === 'angle1') { + if (!Validator.validateTime(suRow[result[key]])) { validRow = false; return; } paramOutput[key] = UnitConverter.getAngleOutput(suRow[result[key]],false); - }else if(key === 'angle2'){ - if(!Validator.validateAngle(suRow[result[key]])){ + } else if (key === 'angle2'){ + if (!Validator.validateAngle(suRow[result[key]])){ validRow = false; return; } paramOutput[key] = UnitConverter.getAngleOutput(suRow[result[key]],true); - }else{ + } else { paramOutput[key] = suRow[result[key]]; } }) paramsOutput['param_'+index] = paramOutput; index++; } - if(!validRow){ + if (!validRow){ continue; } observStrategy.template.parameters.forEach(async(param, index) => { $refs.set(observStrategy.template.parameters[index]['refs'][0], paramsOutput['param_' + index]); }); - if(suRow.id >0 && suRow.suname.length>0 && suRow.sudesc.length>0){ + + //Stations + let sgCellValue = suRow.stations; + let tmpStationGroups = []; + if (sgCellValue && sgCellValue.length >0){ + tmpStationGroups = []; + let tmpStationGroup = {}; + let stationGroups = _.split(sgCellValue, "|"); + stationGroups.map(stationGroup =>{ + tmpStationGroup = {}; + let sgValue = _.split(stationGroup, ":"); + if (sgValue && sgValue[0].length>0){ + let stationArray = _.split(sgValue[0], ","); + + tmpStationGroup['stations'] = stationArray; + tmpStationGroup['max_nr_missing'] = sgValue[1]; + tmpStationGroups.push(tmpStationGroup); + } + + }) + for (const taskName in observStrategy.template.tasks) { + let task = observStrategy.template.tasks[taskName]; + if (task.specifications_doc.station_groups) { + task.specifications_doc.station_groups = tmpStationGroups; + } + } + } + + let between = this.getBetWeenDateValue(suRow.between); + let notbetween = this.getBetWeenDateValue(suRow.notbetween); + + let isNewConstraint = false; + let newConstraint = {}; + let constraint = null; + if (suRow.id >0){ + newSU = _.find(this.state.schedulingUnitList, {'id': suRow.id}); + constraint = newSU.scheduling_constraints_doc; + } + + if ( constraint === null || constraint === 'undefined' || constraint === {}){ + constraint = this.state.schedulingConstraintsDoc; + isNewConstraint = true; + } + + //If No SU Constraint create default ( maintan default struc) + constraint['scheduler'] = suRow.scheduler; + if (suRow.scheduler === 'online'){ + if (!constraint.time.at){ + delete constraint.time.at; + } + if (!constraint.time.after) { + delete constraint.time.after; + } + if (!constraint.time.before) { + delete constraint.time.before; + } + } else { + constraint.time.at = `${moment(suRow.timeat).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; + constraint.time.after = `${moment(suRow.timeafter).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; + constraint.time.before = `${moment(suRow.timebefore).format("YYYY-MM-DDTHH:mm:ss.SSSSS", { trim: false })}Z`; + } + if (between && between.length>0){ + constraint.time.between = between; + } + if (notbetween && notbetween.length>0){ + constraint.time.not_between = notbetween; + } + let dailyValueSelected = _.split(suRow.daily, ","); + this.state.daily.forEach(daily =>{ + if (_.includes(dailyValueSelected, daily)){ + constraint.daily[daily] = true; + } else { + constraint.daily[daily] = false; + } + }) + let min_distance_res = {}; + min_distance_res['sun'] = suRow.md_sun; + min_distance_res['moon'] = suRow.md_moon; + min_distance_res['jupiter'] = suRow.md_jupiter; + constraint.sky.min_distance = min_distance_res; + + let transit_offset_res = {}; + transit_offset_res['from'] = +suRow.offset_from; + transit_offset_res['to'] = +suRow.offset_to; + if (transit_offset_res){ + constraint.sky.transit_offset= transit_offset_res; + } + + constraint.sky.min_target_elevation = suRow.min_target_elevation; + constraint.sky.min_calibrator_elevation = suRow.min_calibrator_elevation; + + UnitConversion.degreeToRadians(constraint.sky); + if (isNewConstraint){ + newSU.scheduling_constraints_doc = constraint; + } + + if (suRow.id === 0){ + newConstraint['scheduling_constraints_doc'] = constraint; + newConstraint['id'] = this.state.constraintId; + newConstraint['constraint'] = {'url':''}; + newConstraint.constraint.url = this.state.constraintUrl; + } + + if (suRow.id >0 && suRow.suname.length>0 && suRow.sudesc.length>0){ newSU = _.find(this.state.schedulingUnitList, {'id': suRow.id}); newSU['name'] = suRow.suname; newSU['description'] = suRow.sudesc; + newSU.requirements_doc.tasks= observStrategy.template.tasks; await ScheduleService.updateSUDraftFromObservStrategy(observStrategy, newSU, this.state.taskDrafts, this.state.tasksToUpdate); existingSUCount++; } - else if(suRow.id === 0 && suRow.suname.length>0 && suRow.sudesc.length>0){ + else if (suRow.id === 0 && suRow.suname.length>0 && suRow.sudesc.length>0){ newSU['id'] = suRow.id; newSU['name'] = suRow.suname; newSU['description'] = suRow.sudesc; - await ScheduleService.saveSUDraftFromObservStrategy(observStrategy, newSU); + await ScheduleService.saveSUDraftFromObservStrategy(observStrategy, newSU, newConstraint); newSUCount++; } } - if((newSUCount+existingSUCount)>0){ + if ((newSUCount+existingSUCount)>0){ const dialog = {header: 'Success', detail: '['+newSUCount+'] Scheduling Units are created & ['+existingSUCount+'] Scheduling Units are updated successfully.'}; this.setState({ dialogVisible: true, dialog: dialog}); - }else{ + } else { this.growl.show({severity: 'error', summary: 'Warning', detail: 'No Scheduling Units create/update '}); } }catch(err){ this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to create/update Scheduling Units'}); } } - + + /** + * Convert the date to string value for Between And Not-Between Columns + * @param {*} dates + */ + getBetweenStringValue(dates){ + let returnDate = ''; + if (dates){ + dates.forEach(utcDateArray =>{ + returnDate +=moment.utc(utcDateArray.from).format(DATE_TIME_FORMAT)+","; + returnDate +=moment.utc(utcDateArray.to).format(DATE_TIME_FORMAT)+"|"; + }) + } + return returnDate; + } + + /** + * convert String to Date value for Between And Not-Between Columns + */ + getBetWeenDateValue(betweenValue){ + let returnDate = []; + if (betweenValue){ + let rowDateArray = _.split(betweenValue, "|"); + rowDateArray.forEach(betweenDates =>{ + let betweendate = _.split(betweenDates, ","); + let dateres = {}; + if (betweendate && betweendate.length === 2){ + dateres['from'] = `${moment(betweendate[0]).format("YYYY-MM-DDTHH:mm:SS.SSSSS", { trim: false })}Z`; + dateres['to'] = `${moment(betweendate[1]).format("YYYY-MM-DDTHH:mm:SS.SSSSS", { trim: false })}Z`; + returnDate.push(dateres); + } + }) + } + return returnDate; + } + + /** * Refresh the grid with updated data */ @@ -702,17 +1339,46 @@ export class SchedulingSetCreate extends Component { } async setNoOfSUint(value){ - if(value >= 0 && value < 501){ + this.setState({isAGLoading: true}); + if (value >= 0 && value < 501){ await this.setState({ noOfSU: value }) - }else{ + } else { await this.setState({ noOfSU: 500 }) } - //refresh row data - await this.prepareScheduleUnitListForGrid(); + + let noOfSU = this.state.noOfSU; + this.tmpRowData = []; + let totalCount = this.state.totalCount; + if (this.state.rowData && this.state.rowData.length >0 && this.state.emptyRow) { + if (this.state.totalCount <= noOfSU) { + // set API data + for (var i = 0; i < totalCount; i++) { + this.tmpRowData.push(this.state.rowData[i]); + } + // add empty row + for(var i = this.state.totalCount; i < noOfSU; i++) { + this.tmpRowData.push(this.state.emptyRow); + } + this.setState({ + rowData: this.tmpRowData, + noOfSU: noOfSU, + isAGLoading: false + }); + } else { + this.setState({ + isAGLoading: false + }) + } + + } else { + this.setState({ + isAGLoading: false + }); + } } validateForm(fieldName) { @@ -755,6 +1421,10 @@ export class SchedulingSetCreate extends Component { return validForm; } + close(){ + this.setState({saveDialogVisible: false}) + } + /** * This function is mainly added for Unit Tests. If this function is removed Unit Tests will fail. */ @@ -762,6 +1432,17 @@ export class SchedulingSetCreate extends Component { return this.validEditor?true:false; } + /** + * Show the content in custom dialog + */ + showDialogContent(){ + return <> Invalid Rows:- Row # and Invalid columns, <br/>{this.state.errorDisplay && this.state.errorDisplay.length>0 && + this.state.errorDisplay.map((msg, index) => ( + <React.Fragment key={index+10} className="col-lg-9 col-md-9 col-sm-12"> + <span key={'label1-'+ index}>{msg}</span> <br /> + </React.Fragment> + ))} </> + } render() { if (this.state.redirect) { @@ -798,7 +1479,7 @@ export class SchedulingSetCreate extends Component { tooltip="Scheduling set of the project" tooltipOptions={this.tooltipOptions} value={this.state.schedulingUnit.scheduling_set_id} options={this.state.schedulingSets} - onChange={(e) => {this.setSchedingSetParams('scheduling_set_id',e.value)}} + onChange={(e) => {this.setSchedulingSetParams('scheduling_set_id',e.value)}} placeholder="Select Scheduling Set" /> <label className={this.state.errors.scheduling_set_id ?"error":"info"}> {this.state.errors.scheduling_set_id ? this.state.errors.scheduling_set_id : "Scheduling Set of the Project"} @@ -833,27 +1514,30 @@ export class SchedulingSetCreate extends Component { </div> </div> <> - {this.state.observStrategy.id && - <div className="ag-theme-alpine" style={ { height: '500px', marginBottom: '10px' } } onKeyDown={this.clipboardEvent}> - <AgGridReact - suppressClipboardPaste={false} - columnDefs={this.state.columnDefs} - columnTypes={this.state.columnTypes} - defaultColDef={this.state.defaultColDef} - rowSelection={this.state.rowSelection} - onGridReady={this.onGridReady} - rowData={this.state.rowData} - frameworkComponents={this.state.frameworkComponents} - context={this.state.context} - components={this.state.components} - modules={this.state.modules} - enableRangeSelection={true} - rowSelection={this.state.rowSelection} - > - - </AgGridReact> - </div> - } + { this.state.isAGLoading ? <AppLoader /> : + <> + {this.state.observStrategy.id && + <div className="ag-theme-alpine" style={ {overflowX: 'inherit !importent', height: '500px', marginBottom: '10px' } } onKeyDown={this.clipboardEvent}> + <AgGridReact + suppressClipboardPaste={false} + columnDefs={this.state.columnDefs} + columnTypes={this.state.columnTypes} + defaultColDef={this.state.defaultColDef} + rowSelection={this.state.rowSelection} + onGridReady={this.onGridReady} + rowData={this.state.rowData} + frameworkComponents={this.state.frameworkComponents} + context={this.state.context} + components={this.state.components} + modules={this.state.modules} + enableRangeSelection={true} + rowSelection={this.state.rowSelection} + > + </AgGridReact> + </div> + } + </> + } </> <div className="p-grid p-justify-start"> <div className="p-col-1"> @@ -864,6 +1548,7 @@ export class SchedulingSetCreate extends Component { <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> </div> </div> + </div> </> } @@ -886,6 +1571,12 @@ export class SchedulingSetCreate extends Component { </div> </Dialog> </div> + + <CustomDialog type="confirmation" visible={this.state.saveDialogVisible} width="40vw" + header={'Save Scheduling Unit(s)'} message={' Some of the Scheduling Unit(s) has invalid data, Do you want to ignore and save valid Scheduling Unit(s) only?'} + content={this.showDialogContent} onClose={this.close} onCancel={this.close} onSubmit={this.saveSU}> + </CustomDialog> + </React.Fragment> ); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js index bb42ed07d8457ef4ea82b9364532acee22d68055..53b1672422eac8ec3525cf2742413f98582656c8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/summary.js @@ -5,6 +5,7 @@ import _ from 'lodash'; import ViewTable from '../../components/ViewTable'; import { JsonToTable } from "react-json-to-table"; import SchedulingConstraints from './Scheduling.Constraints'; +import Stations from './Stations'; /** * Component to view summary of the scheduling unit with limited task details @@ -15,7 +16,7 @@ export class SchedulingUnitSummary extends Component { super(props); this.state = { schedulingUnit: props.schedulingUnit || null - } + }; this.constraintsOrder = ['scheduler','time','daily','sky']; this.closeSUDets = this.closeSUDets.bind(this); this.setConstraintsEditorOutput = this.setConstraintsEditorOutput.bind(this); @@ -152,6 +153,20 @@ export class SchedulingUnitSummary extends Component { } </> } + + {/* {<Stations + stationGroup={this.props.stationGroup} + view + isSummary + />} */} + <div className="col-12"><label>Stations:</label></div> + <div className="col-12 station-list"> + {this.props.stationGroup && this.props.stationGroup.map((station, index) => ( + <div key={`stn-${index}`}>{station}</div> + ))} + + </div> + <div className="col-12 task-summary"> <label>Tasks:</label> <ViewTable 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 9570af1752b60c4a2e16b717eb3a48228fb82879..9a0b6cc381d3dde261eaeca7a05272e03b962bac 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -43,7 +43,8 @@ export class TimelineView extends Component { canShrinkSUList: false, selectedItem: null, suTaskList:[], - isSummaryLoading: false + isSummaryLoading: false, + stationGroup: [] } this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable']; // Statuses before scheduled to get station_group this.allStationsGroup = []; @@ -149,7 +150,7 @@ export class TimelineView extends Component { canExtendSUList: false, canShrinkSUList:false}); if (fetchDetails) { const suBlueprint = _.find(this.state.suBlueprints, {id: (this.state.stationView?parseInt(item.id.split('-')[0]):item.id)}); - ScheduleService.getTaskSubTaskBlueprintsBySchedulingUnit(suBlueprint, true) + ScheduleService.getTaskBPWithSubtaskTemplateOfSU(suBlueprint) .then(taskList => { for (let task of taskList) { //Control Task Id @@ -160,7 +161,8 @@ export class TimelineView extends Component { task.band = task.specifications_doc.filter; } } - this.setState({suTaskList: _.sortBy(taskList, "id"), isSummaryLoading: false}) + this.setState({suTaskList: _.sortBy(taskList, "id"), isSummaryLoading: false, + stationGroup: this.getSUStations(suBlueprint)}); }); // Get the scheduling constraint template of the selected SU block ScheduleService.getSchedulingConstraintTemplate(suBlueprint.suDraft.scheduling_constraints_template_id) @@ -222,37 +224,44 @@ export class TimelineView extends Component { * @param {Array} items */ getStationItemGroups(suBlueprint, timelineItem, group, items) { - /** Get all observation tasks */ - const observtionTasks = _.filter(suBlueprint.tasks, (task) => { return task.template.type_value.toLowerCase() === "observation"}); + /* Get stations based on SU status */ + let stations = this.getSUStations(suBlueprint); + + /* Group the items by station */ + for (const station of stations) { + let stationItem = _.cloneDeep(timelineItem); + stationItem.id = `${stationItem.id}-${station}`; + stationItem.group = station; + items.push(stationItem); + } + } + + /** + * Get all stations of the SU bleprint from the observation task or subtask bases on the SU status. + * @param {Object} suBlueprint + */ + getSUStations(suBlueprint) { let stations = []; - for (const observtionTask of observtionTasks) { + /* Get all observation tasks */ + const observationTasks = _.filter(suBlueprint.tasks, (task) => { return task.template.type_value.toLowerCase() === "observation"}); + for (const observationTask of observationTasks) { /** If the status of SU is before scheduled, get all stations from the station_groups from the task specification_docs */ if (this.STATUS_BEFORE_SCHEDULED.indexOf(suBlueprint.status.toLowerCase()) >= 0 - && observtionTask.specifications_doc.station_groups) { - for (const grpStations of _.map(observtionTask.specifications_doc.station_groups, "stations")) { + && observationTask.specifications_doc.station_groups) { + for (const grpStations of _.map(observationTask.specifications_doc.station_groups, "stations")) { stations = _.concat(stations, grpStations); } } else if (this.STATUS_BEFORE_SCHEDULED.indexOf(suBlueprint.status.toLowerCase()) < 0 - && observtionTask.subTasks) { + && observationTask.subTasks) { /** If the status of SU is scheduled or after get the stations from the subtask specification tasks */ - for (const subtask of observtionTask.subTasks) { + for (const subtask of observationTask.subTasks) { if (subtask.specifications_doc.stations) { stations = _.concat(stations, subtask.specifications_doc.stations.station_list); } } } } - stations = _.uniq(stations); - /** Group the items by station */ - for (const station of stations) { - let stationItem = _.cloneDeep(timelineItem); - stationItem.id = `${stationItem.id}-${station}`; - stationItem.group = station; - items.push(stationItem); - // if (!_.find(group, {'id': station})) { - // group.push({'id': station, title: station}); - // } - } + return _.uniq(stations); } /** @@ -372,6 +381,7 @@ export class TimelineView extends Component { {this.state.isSummaryLoading?<AppLoader /> : <SchedulingUnitSummary schedulingUnit={suBlueprint} suTaskList={this.state.suTaskList} constraintsTemplate={this.state.suConstraintTemplate} + stationGroup={this.state.stationGroup} closeCallback={this.closeSUDets}></SchedulingUnitSummary> } </div> 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 452f57ec512e57c69e141ec18336838d34d85982..67e9ef5c7e7439c1fc5c4399014a7bc4ac9126f4 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 @@ -44,7 +44,8 @@ export class WeekTimelineView extends Component { canShrinkSUList: false, selectedItem: null, suTaskList:[], - isSummaryLoading: false + isSummaryLoading: false, + stationGroup: [] } this.onItemClick = this.onItemClick.bind(this); @@ -166,8 +167,9 @@ export class WeekTimelineView extends Component { canExtendSUList: false, canShrinkSUList:false}); if (fetchDetails) { const suBlueprint = _.find(this.state.suBlueprints, {id: parseInt(item.id.split('-')[0])}); - ScheduleService.getTaskSubTaskBlueprintsBySchedulingUnit(suBlueprint) + ScheduleService.getTaskBPWithSubtaskTemplateOfSU(suBlueprint) .then(taskList => { + const observationTask = _.find(taskList, (task)=> {return task.template.type_value==='observation' && task.specifications_doc.station_groups}); for (let task of taskList) { //Control Task ID const subTaskIds = (task.subTasks || []).filter(sTask => sTask.subTaskTemplate.name.indexOf('control') > 1); @@ -177,7 +179,15 @@ export class WeekTimelineView extends Component { task.band = task.specifications_doc.filter; } } - this.setState({suTaskList: _.sortBy(taskList, "id"), isSummaryLoading: false}) + let stations = []; + //>>>>>> TODO: Station groups from subtasks based on the status of SU + if (observationTask) { + for (const grpStations of _.map(observationTask.specifications_doc.station_groups, "stations")) { + stations = _.concat(stations, grpStations); + } + } + this.setState({suTaskList: _.sortBy(taskList, "id"), isSummaryLoading: false, + stationGroup: _.uniq(stations)}) }); // Get the scheduling constraint template of the selected SU block ScheduleService.getSchedulingConstraintTemplate(suBlueprint.suDraft.scheduling_constraints_template_id) @@ -369,6 +379,7 @@ export class WeekTimelineView extends Component { <SchedulingUnitSummary schedulingUnit={suBlueprint} suTaskList={this.state.suTaskList} constraintsTemplate={this.state.suConstraintTemplate} closeCallback={this.closeSUDets} + stationGroup={this.state.stationGroup} location={this.props.location}></SchedulingUnitSummary> } </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/QAreporting.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/QAreporting.js deleted file mode 100644 index 5ffe01ea22288a4b72aa794753358c400d8862c6..0000000000000000000000000000000000000000 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/QAreporting.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, { Component } from 'react'; -import PageHeader from '../../layout/components/PageHeader'; -import {Growl} from 'primereact/components/growl/Growl'; -import { Button } from 'primereact/button'; -// import AppLoader from '../../layout/components/AppLoader'; -import SunEditor from 'suneditor-react'; -import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File -import {Dropdown} from 'primereact/dropdown'; -// import {InputText} from 'primereact/inputtext'; -import ScheduleService from '../../services/schedule.service'; -import { Link } from 'react-router-dom'; - -class QAreporting extends Component{ - - constructor(props) { - super(props); - this.state={}; - } - - componentDidMount() { - ScheduleService.getSchedulingUnitBlueprintById(this.props.match.params.id) - .then(schedulingUnit => { - this.setState({schedulingUnit: schedulingUnit}); - }) - } - - render() { - return ( - <React.Fragment> - <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'QA Reporting (TO)'} actions={[{icon:'fa-window-close',link:this.props.history.goBack, title:'Click to Close Workflow', props:{ pathname: '/schedulingunit/view'}}]}/> - {this.state.schedulingUnit && - <> - <div> - <div className="p-fluid"> - <div className="p-field p-grid"> - <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> - <div className="col-lg-3 col-md-3 col-sm-12"> - <Link to={ { pathname:`/schedulingunit/view/blueprint/${this.state.schedulingUnit.id}`}}>{this.state.schedulingUnit.name}</Link> - </div> - <div className="col-lg-1 col-md-1 col-sm-12"></div> - <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit Status</label> - <div className="col-lg-3 col-md-3 col-sm-12"> - {/* <InputText id="suStatus" data-testid="name" disabled - value={this.state.schedulingUnit.status}/> */} - <span>{this.state.schedulingUnit.status}</span> - </div> - </div> - <div className="p-field p-grid"> - <label htmlFor="assignTo" className="col-lg-2 col-md-2 col-sm-12">Assign To </label> - <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > - <Dropdown inputId="projCat" optionLabel="value" optionValue="value" - options={[{value: 'User 1'},{value: 'User 2'},{value: 'User 3'}]} - placeholder="Assign To" /> - </div> - <div className="col-lg-1 col-md-1 col-sm-12"></div> - <label htmlFor="viewPlots" className="col-lg-2 col-md-2 col-sm-12">View Plots</label> - <div className="col-lg-3 col-md-3 col-sm-12" style={{paddingLeft:'2px'}}> - <label className="col-sm-10 " > - <a href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> - </label> - <label className="col-sm-10 "> - <a href="https://proxy.lofar.eu/qa" target="_blank">Adder plots</a> - </label> - <label className="col-sm-10 "> - <a href=" https://proxy.lofar.eu/lofmonitor/" target="_blank">Station Monitor</a> - </label> - </div> - </div> - <div className="p-grid" style={{padding: '10px'}}> - <label htmlFor="comments" >Comments</label> - <div className="col-lg-12 col-md-12 col-sm-12"></div> - <SunEditor height="250" enableToolbar={true} - setOptions={{ - buttonList: [ - ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video','italic', 'strike', 'subscript', - 'superscript','outdent', 'indent','fullScreen', 'showBlocks', 'codeView','preview', 'print','removeFormat'] - ] - }} /> - </div> - </div> - <div className="p-grid" style={{marginTop: '20px'}}> - <div className="p-col-1"> - <Button label="Save" className="p-button-primary" icon="pi pi-check" /> - </div> - <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" /> - </div> - </div> - - </div> - </> - } - </React.Fragment> - )}; -} -export default QAreporting; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js new file mode 100644 index 0000000000000000000000000000000000000000..5e1cad1b4c4994b239b627675ee6f85e5fc04891 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/Scheduled.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import { Button } from 'primereact/button'; +import { Link } from 'react-router-dom'; +import moment from 'moment'; + +class Scheduled extends Component { + constructor(props) { + super(props); + this.state = {}; + this.Next = this.Next.bind(this); + } + + /** + * Method will trigger on click save buton + * here onNext props coming from parent, where will handle redirection to other page + */ + Next() { + this.props.onNext({}); + } + + render() { + return ( + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Start Time</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <span>{this.props.schedulingUnit.start_time && moment(this.props.schedulingUnit.start_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">End Time</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <span>{this.props.schedulingUnit.stop_time && moment(this.props.schedulingUnit.stop_time).format("YYYY-MMM-DD HH:mm:SS")}</span> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Timeline</label> + <div className="col-lg-3 col-md-3 col-sm-12 block-list"> + <Link to={{ pathname: '/su/timelineview' }}>TimeLine View <span class="fas fa-clock"></span></Link> + <Link to={{ pathname: '/su/timelineview/week' }}>Week Overview <span class="fas fa-calendar-alt"></span></Link> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Scheduling Method</label> + <div className="col-lg-3 col-md-3 col-sm-12 block-list"> + <span>{this.props.schedulingUnit.scheduling_constraints_doc.scheduler}</span> + </div> + </div> + </div> + + <div className="p-grid p-justify-start"> + <div className="p-col-1"> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width: '90px' }} /> + </div> + </div> + </div> + </> + ) + }; + +} +export default Scheduled; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js new file mode 100644 index 0000000000000000000000000000000000000000..7087513f513adf2350dc8fd911b7d9c7953da671 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/decide.acceptance.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react'; +import { Button } from 'primereact/button'; +import SunEditor from 'suneditor-react'; +import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File +import { Checkbox } from 'primereact/checkbox'; + +class DecideAcceptance extends Component { + constructor(props) { + super(props); + this.state = { + content: props.report, + picomment: props.picomment, //PI Comment Field + showEditor: false, //Sun Editor + checked: false, //Checkbox + + }; + this.Next = this.Next.bind(this); + this.handleChange = this.handleChange.bind(this); + this.onChangePIComment = this.onChangePIComment.bind(this); + } + + + // Method will trigger on change of operator report sun-editor + handleChange(e) { + this.setState({ + content: e + }); + localStorage.setItem('report_qa', e); + } + + //PI Comment Editor + onChangePIComment(e) { + this.setState({ + picomment: e.target.value + }); + localStorage.setItem('pi_comment', e.target.value); + } + + /** + * Method will trigger on click save buton + * here onNext props coming from parent, where will handle redirection to other page + */ + Next() { + this.props.onNext({ + report: this.state.content, + picomment: this.state.picomment + + }); + } + + // Not using at present + cancelCreate() { + this.props.history.goBack(); + } + + render() { + return ( + <> + <div> + <div className="p-fluid"> + <div className="p-grid" style={{ padding: '15px' }}> + <label htmlFor="operatorReport" >Operator Report</label> + <div className="col-lg-12 col-md-12 col-sm-12"></div> + {this.state.showEditor && <SunEditor setDefaultStyle="min-height: 250px; height: auto;" enableToolbar={true} + onChange={this.handleChange} + setContents={this.state.content} + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', + 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] + ] + }} + />} + <div dangerouslySetInnerHTML={{ __html: this.state.content }}></div> + </div> + <div className="p-field p-grid"> + <label htmlFor="piReport" className="col-lg-2 col-md-2 col-sm-12">PI Report</label> + <div className="col-lg-12 col-md-12 col-sm-12"> + {this.state.showEditor && <SunEditor setDefaultStyle="min-height: 250px; height: auto;" enableToolbar={true} + onChange={this.onChangePIComment} + setContents={this.state.picomment} + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', + 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] + ] + }} + />} + <div className="operator-report" dangerouslySetInnerHTML={{ __html: this.state.picomment }}></div> + </div> + </div> + <div className="p-field p-grid"> + <label htmlFor="piAccept" className="col-lg-2 col-md-2 col-sm-12">SDCO accepts after PI</label> + <div className="col-lg-3 col-md-3 col-sm-6"> + <div className="p-field-checkbox"> + <Checkbox inputId="binary" checked={this.state.checked} onChange={e => this.setState({ checked: e.checked })} /> + </div> + </div> + </div> + </div> + <div className="p-grid" style={{ marginTop: '20px' }}> + <div className="p-col-1"> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick = { this.Next } /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + </div> + </div> + + </div> + </> + ) + }; +} +export default DecideAcceptance; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f477d2f5970a6e42ae33cc5cef7d92c16af638e2 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/index.js @@ -0,0 +1,93 @@ +import React, { useEffect, useState } from 'react'; +import PageHeader from '../../layout/components/PageHeader'; +import {Growl} from 'primereact/components/growl/Growl'; +import { Link } from 'react-router-dom'; +import ScheduleService from '../../services/schedule.service'; +import Scheduled from './Scheduled'; +import ProcessingDone from './processing.done'; +import QAreporting from './qa.reporting'; +import QAsos from './qa.sos'; +import PIverification from './pi.verification'; +import DecideAcceptance from './decide.acceptance'; +import IngestDone from './ingest.done'; + +//Workflow Page Title +const pageTitle = ['Scheduled','Processing Done','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingest Done']; + +export default (props) => { + let growl; + const [state, setState] = useState({}); + const [currentStep, setCurrentStep] = useState(1); + const [schedulingUnit, setSchedulingUnit] = useState(); + const [ingestTask, setInjestTask] = useState({}); + useEffect(() => { + // Clearing Localstorage on start of the page to load fresh + clearLocalStorage(); + ScheduleService.getSchedulingUnitBlueprintById(props.match.params.id) + .then(schedulingUnit => { + setSchedulingUnit(schedulingUnit); + }) + const promises = [ScheduleService.getSchedulingUnitBlueprintById(props.match.params.id), ScheduleService.getTaskType()] + Promise.all(promises).then(responses => { + setSchedulingUnit(responses[0]); + ScheduleService.getTaskBlueprintsBySchedulingUnit(responses[0], true, false).then(response => { + setInjestTask(response.find(task => task.template.type_value==='observation')); + }); + }); + }, []); + + const clearLocalStorage = () => { + localStorage.removeItem('pi_comment'); + localStorage.removeItem('report_qa'); + } + + //Pages changes step by step + const onNext = (content) => { + setState({...state, ...content}); + setCurrentStep(currentStep + 1); + }; + + return ( + <> + <Growl ref={(el) => growl = el} /> + <PageHeader location={props.location} title={`${pageTitle[currentStep - 1]}`} actions={[{ icon: 'fa-window-close', link: props.history.goBack, title: 'Click to Close Workflow', props: { pathname: '/schedulingunit/1/workflow' } }]} /> + {schedulingUnit && + <> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="suName" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Link to={{ pathname: `/schedulingunit/view/blueprint/${schedulingUnit.id}` }}>{schedulingUnit.name}</Link> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="suStatus" className="col-lg-2 col-md-2 col-sm-12">Scheduling Unit Status</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <span>{schedulingUnit.status}</span> + </div> + <label htmlFor="viewPlots" className="col-lg-2 col-md-2 col-sm-12">View Plots</label> + <div className="col-lg-3 col-md-3 col-sm-12" style={{ paddingLeft: '2px' }}> + <label className="col-sm-10 " > + <a href="https://proxy.lofar.eu/inspect/HTML/" target="_blank">Inspection plots</a> + </label> + <label className="col-sm-10 "> + <a href="https://proxy.lofar.eu/qa" target="_blank">Adder plots</a> + </label> + <label className="col-sm-10 "> + <a href=" https://proxy.lofar.eu/lofmonitor/" target="_blank">Station Monitor</a> + </label> + </div> + </div> + {currentStep === 1 && <Scheduled onNext={onNext} {...state} schedulingUnit={schedulingUnit} />} + {currentStep === 2 && <ProcessingDone onNext={onNext} {...state}/>} + {currentStep === 3 && <QAreporting onNext={onNext}/>} + {currentStep === 4 && <QAsos onNext={onNext} {...state} />} + {currentStep === 5 && <PIverification onNext={onNext} {...state} />} + {currentStep === 6 && <DecideAcceptance onNext={onNext} {...state} />} + {currentStep === 7 && <IngestDone onNext={onNext}{...state} task={ingestTask} />} + + </div> + </> + } + </> + ) +}; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js new file mode 100644 index 0000000000000000000000000000000000000000..db2887b1bff7c3d96fe9b0e3dcca28b3a88be890 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/ingest.done.js @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; + +class IngestDone extends Component { + constructor(props) { + super(props); + this.state = { }; + this.onSave = this.onSave.bind(this); + } + + onSave(){ + this.props.onNext({ + report: this.props.report, + picomment: this.props.picomment + }); + } + + render(){ + return( + <> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="ingestTaskStatus" className="col-lg-2 col-md-2 col-sm-12">Ingest Task Status</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <span>{this.props.task.status}</span> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="ingestTask" className="col-lg-2 col-md-2 col-sm-12">Ingest Task</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <a href={`${window.location.origin}/task/view/blueprint/${this.props.task.id}`}>{this.props.task.name}</a> + </div> + <label htmlFor="ingestMonitoring" className="col-lg-2 col-md-2 col-sm-12">Ingest Monitoring</label> + <label className="col-sm-10 " > + <a href="http://lexar003.control.lofar:9632/" target="_blank">View Ingest Monitoring <span class="fas fa-desktop"></span></a> + </label> + + {/* <div className="col-lg-3 col-md-3 col-sm-12"> + <Link to={{ pathname: `http://lexar003.control.lofar:9632/` }}> View Ingest Monitoring <span class="fas fa-desktop"></span></Link> + </div> */} + </div> + </div> + </> + ) + }; + +} +export default IngestDone; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js new file mode 100644 index 0000000000000000000000000000000000000000..f63a6fe0591e6dd6acd9fd5bc72c1988c33fc358 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/pi.verification.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react'; +import { Button } from 'primereact/button'; +import SunEditor from 'suneditor-react'; +import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File +import { Checkbox } from 'primereact/checkbox'; +//import {InputTextarea} from 'primereact/inputtextarea'; + +class PIverification extends Component { + constructor(props) { + super(props); + this.state = { + content: props.report, + showEditor: false, + }; + this.Next = this.Next.bind(this); + this.handleChange = this.handleChange.bind(this); + this.onChangePIComment = this.onChangePIComment.bind(this); + } + + /** + * Method wiill trigger on change of operator report sun-editor + */ + handleChange(e) { + this.setState({ + comment: e + }); + localStorage.setItem('report_pi', e); + } + + /** + * Method will trigger on click save buton + * here onNext props coming from parent, where will handle redirection to other page + */ + Next(){ + this.props.onNext({ + report: this.state.content, + picomment: this.state.comment + }); + } + + /** + * Method wiill triigger on change of pi report sun-editor + */ + onChangePIComment(a) { + this.setState({ + comment: a + }); + localStorage.setItem('comment_pi', a); + } + + // Not using at present + cancelCreate() { + this.props.history.goBack(); + } + + render() { + return ( + <> + <div> + <div className="p-fluid"> + <div className="p-grid" style={{ padding: '10px' }}> + <label htmlFor="operatorReport" >Operator Report</label> + <div className="col-lg-12 col-md-12 col-sm-12"></div> + {this.state.showEditor && <SunEditor setDefaultStyle="min-height: 250px; height: auto;" enableToolbar={true} + onChange={this.handleChange} + setContents={this.state.content} + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', + 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] + ] + }} + />} + <div className="operator-report" dangerouslySetInnerHTML={{ __html: this.state.content }}></div> + </div> + <div className="p-grid" style={{ padding: '10px' }}> + <label htmlFor="piReport" >PI Report</label> + <div className="col-lg-12 col-md-12 col-sm-12"></div> + <SunEditor setDefaultStyle="min-height: 150px; height: auto;" enableToolbar={true} + setContents={this.state.comment} + onChange={this.onChangePIComment} + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', + 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] + ] + }} /> + {/* <InputTextarea rows={3} cols={30} + tooltip="PIReport" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="PIReport" + value={this.state.piComment} + onChange={this.onChangePIComment} + /> */} + </div> + <div className="p-field p-grid"> + <label htmlFor="piAccept" className="col-lg-2 col-md-2 col-sm-12">PI Accept</label> + <div className="p-field-checkbox"> + <Checkbox inputId="binary" checked={this.state.checked} onChange={e => this.setState({ checked: e.checked })} /> + </div> + </div> + <div className="p-grid" style={{ marginTop: '20px' }}> + <div className="p-col-1"> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + </div> + </div> + </div> + </div> + </> + ) + }; +} +export default PIverification; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js new file mode 100644 index 0000000000000000000000000000000000000000..07c7ca9cdca1ae96d2d8ce166d836303036ccbc0 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/processing.done.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import { Button } from 'primereact/button'; + +class ProcessingDone extends Component { + + constructor(props) { + super(props); + this.Next = this.Next.bind(this); + } + + /** + * Method will trigger on click save buton + * here onNext props coming from parent, where will handle redirection to other page + */ + Next(){ + this.props.onNext({}); + } + + render(){ + return( + <> + <div className="p-grid p-justify-start"> + <div className="p-col-1"> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next }/> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + </div> + </div> + + </> + ) + }; + + +} +export default ProcessingDone \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js new file mode 100644 index 0000000000000000000000000000000000000000..1c4684e2c5b5b13e34cd1a2f87a27118c27f33f8 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.reporting.js @@ -0,0 +1,80 @@ +import React, { Component } from 'react'; +import { Button } from 'primereact/button'; +import SunEditor from 'suneditor-react'; +import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File +import { Dropdown } from 'primereact/dropdown'; +//import katex from 'katex' // for mathematical operations on sun editor this component should be added +//import 'katex/dist/katex.min.css' + +class QAreporting extends Component{ + + constructor(props) { + super(props); + this.state={ + content: props.report + }; + this.Next = this.Next.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + /** + * Method will trigger on click save buton + * here onNext props coming from parent, where will handle redirection to other page + */ + Next() { + this.props.onNext({ report: this.state.content }); + } + + /** + * Method will trigger on change of operator report sun-editor + */ + handleChange(e) { + localStorage.setItem('report_qa', e); + this.setState({ content: e }); + } + + //Not using at present + cancelCreate() { + this.props.history.goBack(); + } + + render() { + return ( + <> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="assignTo" className="col-lg-2 col-md-2 col-sm-12">Assign To </label> + <div className="col-lg-3 col-md-3 col-sm-12" data-testid="assignTo" > + <Dropdown inputId="assignToValue" optionLabel="value" optionValue="value" + options={[{ value: 'User 1' }, { value: 'User 2' }, { value: 'User 3' }]} + placeholder="Assign To" /> + </div> + </div> + <div className="p-grid" style={{ padding: '10px' }}> + <label htmlFor="comments" >Comments</label> + <div className="col-lg-12 col-md-12 col-sm-12"></div> + <SunEditor enableToolbar={true} + setDefaultStyle="min-height: 250px; height: auto;" + onChange={ this.handleChange } + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', + 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] + ] + }} /> + </div> + </div> + <div className="p-grid p-justify-start"> + <div className="p-col-1"> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '88px' }}/> + </div> + </div> + </> + ) +}; + +} +export default QAreporting; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js new file mode 100644 index 0000000000000000000000000000000000000000..59ef61e29ac4d7f2fe3eb7510fc290e65febff47 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/qa.sos.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import { Button } from 'primereact/button'; +import SunEditor from 'suneditor-react'; +import 'suneditor/dist/css/suneditor.min.css'; // Import Sun Editor's CSS File +import { Checkbox } from 'primereact/checkbox'; + +class QAreportingSDCO extends Component { + constructor(props) { + super(props); + this.state = { + content: props.report, + showEditor: false, + checked: false, + pichecked: false + + }; + this.Next = this.Next.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + /** + * Method will trigger on change of sun-editor + */ + handleChange(e) { + this.setState({ + content: e + }); + localStorage.setItem('report_qa', e); + } + + /** + * Method will trigger on click save buton + * here onNext props coming from parent, where will handle redirection to other page + */ + Next() { + this.props.onNext({ + report: this.state.content, + piChecked: this.state.pichecked + }) + } + + //Not using at present + cancelCreate() { + this.props.history.goBack(); + } + + render() { + return ( + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="qualityPolicy" className="col-lg-2 col-md-2 col-sm-12">Quality Policy</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <div className="p-field-checkbox"> + <Checkbox inputId="binary" checked={this.state.checked} onChange={e => this.setState({ checked: e.checked })} /> + </div> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="sdcoAccept" className="col-lg-2 col-md-2 col-sm-12">SDCO Accept</label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <div className="p-field-checkbox"> + <Checkbox inputId="secondary" pichecked={this.state.pichecked} onChange={e => this.setState({ pichecked: e.pichecked })} /> + </div> + </div> + </div> + <div className="p-grid" style={{ padding: '10px' }}> + <label htmlFor="operatorReport" >Operator Report {!this.state.showEditor && <span className="con-edit">(Click content to edit)</span>}</label> + <div className="col-lg-12 col-md-12 col-sm-12"></div> + {this.state.showEditor && <SunEditor setDefaultStyle="min-height: 250px; height: auto" enableToolbar={true} + onChange={this.handleChange} + setContents={this.state.content} + setOptions={{ + buttonList: [ + ['undo', 'redo', 'bold', 'underline', 'fontColor', 'table', 'link', 'image', 'video', 'italic', 'strike', 'subscript', + 'superscript', 'outdent', 'indent', 'fullScreen', 'showBlocks', 'codeView', 'preview', 'print', 'removeFormat'] + ] + }} + />} + {!this.state.showEditor && <div onClick={() => this.setState({ showEditor: !this.state.showEditor })} className="operator-report" dangerouslySetInnerHTML={{ __html: this.state.content }}></div>} + </div> + </div> + <div className="p-grid" style={{ marginTop: '20px' }}> + <div className="p-col-1"> + <Button label="Next" className="p-button-primary" icon="pi pi-check" onClick={ this.Next } /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" style={{ width : '90px' }} /> + </div> + </div> + </div> + </> + ) + }; + +} +export default QAreportingSDCO; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index a5c43e081e64a4c5801d01d50d1ef4ad9d4b35d7..156267acb0fad8c56c76a1c548a491683aacf184 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -2,7 +2,7 @@ import React from 'react'; import { Route, Switch, - Redirect, + // Redirect, } from 'react-router-dom'; import {NotFound} from '../layout/components/NotFound'; @@ -16,13 +16,14 @@ import EditSchedulingUnit from './Scheduling/edit'; import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; import {TimelineView, WeekTimelineView} from './Timeline'; import SchedulingSetCreate from './Scheduling/create.scheduleset'; -import QAreporting from './Workflow/QAreporting'; +import Workflow from './Workflow'; + + export const routes = [ { path: "/not-found", component: NotFound, - },{ path: "/dashboard", component: Dashboard, @@ -154,17 +155,18 @@ export const routes = [ }, { path: "/schedulingunit/:id/workflow", - component: QAreporting, - name: 'QA Reporting (TO)', + component: Workflow, + name: 'Workflow', title: 'QA Reporting (TO)' - } + }, + ]; export const RoutedContent = () => { return ( <Switch> - <Redirect from="/" to="/" exact /> - {routes.map(routeProps => <Route {...routeProps} exact key={routeProps.path} />)} + {/* <Redirect from="/" to="/" exact /> */} + {routes.map(routeProps => <Route {...routeProps} exact key={routeProps.path} />)} </Switch> ); } \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/auth.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/auth.service.js new file mode 100644 index 0000000000000000000000000000000000000000..c6f2f964dab0fb646e110f7956e930d859d5539f --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/auth.service.js @@ -0,0 +1,18 @@ +import Cookies from 'js-cookie'; + +const axios = require('axios'); +delete axios.defaults.headers.common['Authorization']; +const AuthService = { + authenticate: async() => { + try { + console.log(Cookies.get('csrftoken')); + const response = await axios.post("/accounts/login/", {csrfmiddlewaretoken: Cookies.get('csrftoken'), username: "test", password: "test"}); + // const response = await axios.post("/accounts/login/", {username: "test", password: "test"}); + console.log(response); + } catch(error) { + console.error(error); + } + } +} + +export default AuthService; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js index 6e65a0019fe412d16ca82b46e114a9cbc2dd6c92..3e7646d162cbd04337a6b55d24e4677976f1b408 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -69,6 +69,7 @@ const ScheduleService = { schedulingUnit.scheduling_set_id = schedulingUnitDraft.scheduling_set_id; schedulingUnit.scheduling_set = schedulingUnitDraft.scheduling_set; schedulingUnit.scheduling_set_object = schedulingUnitDraft.scheduling_set_object; + schedulingUnit.scheduling_constraints_doc = schedulingUnitDraft.scheduling_constraints_doc; } return schedulingUnit; } catch(error) { @@ -76,6 +77,15 @@ const ScheduleService = { return null; } }, + getTaskType: async function(){ + try { + const response = await axios.get('/api/task_type'); + return response.data.results; + } catch(error) { + console.error(error); + return null; + }; + }, getSchedulingUnitDraftById: async function (id){ try { const schedulingUnit = (await axios.get('/api/scheduling_unit_draft/'+id)).data; @@ -360,7 +370,6 @@ const ScheduleService = { }, updateSchedulingUnitDraft: async function(schedulingUnit) { try { - // console.log(schedulingUnit); schedulingUnit.scheduling_constraints_doc = ( schedulingUnit.scheduling_constraints_doc == null)?"": schedulingUnit.scheduling_constraints_doc; const suUpdateResponse = await axios.put(`/api/scheduling_unit_draft/${schedulingUnit.id}/`, schedulingUnit); return suUpdateResponse.data;