Click here to Skip to main content
15,890,374 members
Articles / Programming Languages / Javascript

Madcap Idea 13: Getting the ‘VIEW JOB’ to Work End-End

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
22 Nov 2017CPOL8 min read 4K   1   1
How to get the 'view job' to work end-end

Last Time

Last time we looked at finishing the “Create Job” page, which creates the initial Job and sends it out via Kafka through the play backend and back to other users browsers via comet/websock and finally some RX.js. This post will see us finish the entire system by implementing the final page “ViewJob”.

PreAmble

Just as a reminder, this is part of my ongoing set of posts which I talk about in this post, where we will be building up to a point where we have a full app using lots of different stuff, such as these:

  • WebPack
  • React.js
  • React Router
  • TypeScript
  • Babel.js
  • Akka
  • Scala
  • Play (Scala Http Stack)
  • MySql
  • SBT
  • Kafka
  • Kafka Streams

Ok, so now that we have the introductions out of the way, let's crack on with what we want to cover in this post.

Where is the Code?

As usual, the code is on GitHub here.

What Is This Post All About?

As stated above, this post deals with the “View Job” functions.

This is kind of a hard post to write, as what I will have to do really is just present a wall of text, as it's all very isolated to one single typescript file with a couple of helper classes. However, what might help is to talk about some of the actions that this wall of text that follows will do.

Is This The End?

Whilst this does indeed wrap up the blog side of things, I intend to do a single http://www.codeproject.com article which will be a highly compressed version of these 13 posts. As such, it may be easier to get the gist of what I have done here than reading these 13 posts.

So What Should the “View Job” Page Do?

This page should do the following things:

  • If a passenger sends out a job, it should be seen by ANY driver that is logged in (providing the job is not already assigned to a driver)
  • Positions updates from passenger to drivers (that know about the passenger) should show the new passenger position
  • When a driver pushes out (single laptop requires that users click on map to make their own position known to others) their new position that the client sees that and updates the driver marker accordingly
  • That a passenger can accept a driver for a job
  • That a driver cannot accept a job from a passenger
  • That once a job is paired between passenger/driver, only those 2 markers will be shown if you are either of these users
  • That once a job is paired between passenger/driver AND YOU ARE NOT ONE OF THESE USERS that you ONLY see your own markers
  • That a job may be completed by passenger OR driver independently and that they are able to “Rate” each other

I think those are the main points. So how about that wall of text.

Wall of Text that Does the Stuffz

Here is the Wall of Text

JavaScript
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as _ from "lodash";
import Measure from 'react-measure'
import { RatingDialog } from "./components/RatingDialog";
import { YesNoDialog } from "./components/YesNoDialog";
import { OkDialog } from "./components/OkDialog";
import { AcceptList } from "./components/AcceptList";
import 'bootstrap/dist/css/bootstrap.css';
import {
    Well,
    Grid,
    Row,
    Col,
    ButtonInput,
    ButtonGroup,
    Button,
    Modal,
    Popover,
    Tooltip,
    OverlayTrigger
} from "react-bootstrap";
import { AuthService } from "./services/AuthService";
import { JobService } from "./services/JobService";
import { JobStreamService } from "./services/JobStreamService";
import { PositionService } from "./services/PositionService";
import { Position } from "./domain/Position";
import { PositionMarker } from "./domain/PositionMarker";
import { hashHistory } from 'react-router';
import { withGoogleMap, GoogleMap, Marker, OverlayView } from "react-google-maps";

const STYLES = {
    overlayView: {
        background: `white`,
        border: `1px solid #ccc`,
        padding: 15,
    }
}

const GetPixelPositionOffset = (width, height) => {
    return { x: -(width / 2), y: -(height / 2) };
}

const ViewJobGoogleMap = withGoogleMap(props => (

    <GoogleMap
        ref={props.onMapLoad}
        defaultZoom={16}
        defaultCenter={{ lat: 50.8202949, lng: -0.1406958 }}
        onClick={props.onMapClick}>
        {props.markers.map((marker, index) => (
            <OverlayView
                key={marker.key}
                mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
                position={marker.position}
                getPixelPositionOffset={GetPixelPositionOffset}>
                <div style={STYLES.overlayView}>
                    <img src={marker.icon} />
                    <strong>{marker.key}</strong>
                </div>
            </OverlayView>
        ))}
    </GoogleMap>
));

export interface ViewJobState {
    markers: Array<PositionMarker>;
    okDialogOpen: boolean;
    okDialogKey: number;
    okDialogHeaderText: string;
    okDialogBodyText: string;
    dimensions: {
        width: number,
        height: number
    },
    currentPosition: Position;
    isJobAccepted: boolean;
    finalActionHasBeenClicked: boolean;
}

type DoneCallback = (jdata: any, textStatus: any, jqXHR: any) => void

export class ViewJob extends React.Component<undefined, ViewJobState> {

    private _authService: AuthService;
    private _jobService: JobService;
    private _jobStreamService: JobStreamService;
    private _positionService: PositionService;
    private _subscription: any; 
    private _currentJobUUID: any;

    constructor(props: any) {
        super(props);
        this._authService = props.route.authService;
        this._jobStreamService = props.route.jobStreamService;
        this._jobService = props.route.jobService;
        this._positionService = props.route.positionService;
        
        if (!this._authService.isAuthenticated()) {
            hashHistory.push('/');
        }

        let savedMarkers: Array<PositionMarker> = new Array<PositionMarker>();
        if (this._positionService.hasJobPositions()) {
            savedMarkers = this._positionService.userJobPositions();
        }

        this.state = {
            markers: savedMarkers,
            okDialogHeaderText: '',
            okDialogBodyText: '',
            okDialogOpen: false,
            okDialogKey: 0,
            dimensions: { width: -1, height: -1 },
            currentPosition: this._authService.isDriver() ? null :
                this._positionService.currentPosition(),
            isJobAccepted: false,
            finalActionHasBeenClicked: false
        };
    }

    componentWillMount() {
        var self = this;
        this._subscription =
            this._jobStreamService.getJobStream()
            .retry()
            .where(function (x, idx, obs) {
                return self.shouldShowMarkerForJob(x.detail);
            })
            .subscribe(
                jobArgs => {

                    console.log('RX saw onJobChanged');
                    console.log('RX x = ', jobArgs.detail);

                    this._jobService.clearUserIssuedJob();
                    this._jobService.storeUserIssuedJob(jobArgs.detail);
                    this.addMarkerForJob(jobArgs.detail);
                },
                error => {
                    console.log('RX saw ERROR');
                    console.log('RX error = ', error);
                },
                () => {
                    console.log('RX saw COMPLETE');
                }
            );
    }

    componentWillUnmount() {
        this._subscription.dispose();
        this._positionService.storeUserJobPositions(this.state.markers);
    }

    render() {

        const adjustedwidth = this.state.dimensions.width;

        return (
            <Well className="outer-well">
                <Grid>
                    <Row className="show-grid">
                        <Col xs={10} md={6}>
                            <h4>CURRENT JOB</h4>
                        </Col>
                    </Row>
                    <Row className="show-grid">
                        <Col xs={10} md={6}>
                            <AcceptList
                                markers={_.filter(this.state.markers, { isDriverIcon: true })}
                                currentUserIsDriver={this._authService.isDriver()}
                                clickCallback={this.handleMarkerClick}
                            />
                        </Col>
                    </Row>
                    <Row className="show-grid">
                        <Col xs={10} md={6}>
                            <Measure
                                bounds
                                onResize={(contentRect) => {
                                    this.setState({ dimensions: contentRect.bounds })
                                }}>
                                {({ measureRef }) =>
                                    <div ref={measureRef}>
                                        <ViewJobGoogleMap
                                            containerElement={
                                                <div style={{
                                                    position: 'relative',
                                                    top: 0,
                                                    left: 0,
                                                    right: 0,
                                                    bottom: 0,
                                                    width: { adjustedwidth },
                                                    height: 600,
                                                    justifyContent: 'flex-end',
                                                    alignItems: 'center',
                                                    marginTop: 20,
                                                    marginLeft: 0,
                                                    marginRight: 0,
                                                    marginBottom: 20
                                                }} />
                                            }
                                            mapElement={
                                                <div style={{
                                                    position: 'relative',
                                                    top: 0,
                                                    left: 0,
                                                    right: 0,
                                                    bottom: 0,
                                                    width: { adjustedwidth },
                                                    height: 600,
                                                    marginTop: 20,
                                                    marginLeft: 0,
                                                    marginRight: 0,
                                                    marginBottom: 20
                                                }} />
                                            }
                                            markers={this.state.markers}
                                            onMapClick={this.handleMapClick}
                                        />
                                    </div>
                                }
                            </Measure>
                        </Col>
                    </Row>

                    {this.state.isJobAccepted === true ?
                        <Row className="show-grid">
                            <span>
                                <RatingDialog
                                    theId="viewJobCompleteBtn"
                                    headerText="Rate your driver/passenger"
                                    okCallBack={this.ratingsDialogOkCallBack}
                                    actionPerformed={this.state.finalActionHasBeenClicked} />

                                {!(this._authService.isDriver() === true) ?

                                    <YesNoDialog
                                        theId="viewJobCancelBtn"
                                        launchButtonText="Cancel"
                                        actionPerformed={this.state.finalActionHasBeenClicked} 
                                        yesCallBack={this.jobCancelledCallBack}
                                        noCallBack={this.jobNotCancelledCallBack}
                                        headerText="Cancel the job" />
                                    : 
                                    null
                                }

                                <OkDialog
                                    open={this.state.okDialogOpen}
                                    okCallBack={this.okDialogCallBack}
                                    headerText={this.state.okDialogHeaderText}
                                    bodyText={this.state.okDialogBodyText}
                                    key={this.state.okDialogKey} />
                            </span>
                        </Row> :
                        null
                    }
                </Grid>
            </Well>
        );
    }

    handleMapClick = (event) => {

        let currentUser = this._authService.user();
        let isDriver = this._authService.isDriver();
        let matchedMarker = _.find(this.state.markers, { 'email': currentUser.email });
        let newPosition = new Position(event.latLng.lat(), event.latLng.lng());
        let currentJob = this._jobService.currentJob();
        this._positionService.clearUserPosition();
        this._positionService.storeUserPosition(newPosition);

        if (matchedMarker != undefined) {
            let newMarkersList = this.state.markers;
            _.remove(newMarkersList, function (n) {
                return n.email === matchedMarker.email;
            });
            matchedMarker.position = newPosition;
            newMarkersList.push(matchedMarker);
            const newState = Object.assign({}, this.state, {
                currentPosition: newPosition,
                markers: newMarkersList
            })
            this.setState(newState);
            currentJob = matchedMarker.jobForMarker;
        }
        else {
            if (isDriver) {
                let newDriverMarker =
                    this.createDriverMarker(currentUser, event);
                let newMarkersList = this.state.markers;
                newMarkersList.push(newDriverMarker);
                const newState = Object.assign({}, this.state, {
                    currentPosition: newPosition,
                    markers: newMarkersList
                })
                this.setState(newState);
            }
        }
        this._positionService.clearUserJobPositions();
        this._positionService.storeUserJobPositions(this.state.markers);
        this.pushOutJob(newPosition, currentJob);
    }

    handleMarkerClick = (targetMarker) => {

        console.log('button on AcceptList clicked:' + targetMarker.key);
        console.log(targetMarker);

        let currentJob = this._jobService.currentJob();
        let jobForMarker = targetMarker.jobForMarker;

        let clientMarker = _.find(this.state.markers, { 'isDriverIcon': false });
        if (clientMarker != undefined && clientMarker != null) {

            let clientJob = clientMarker.jobForMarker;
            clientJob.driverFullName = jobForMarker.driverFullName;
            clientJob.driverEmail = jobForMarker.driverEmail;
            clientJob.driverPosition = jobForMarker.driverPosition;
            clientJob.vehicleDescription = jobForMarker.vehicleDescription;
            clientJob.vehicleRegistrationNumber = jobForMarker.vehicleRegistrationNumber;
            clientJob.isAssigned = true;
            
            let self = this;
            console.log("handleMarkerClick job");
            console.log(clientJob);

            this.makePOSTRequest('job/submit', clientJob, this,
                function (jdata, textStatus, jqXHR) {
                    console.log("After is accepted");
                    const newState = Object.assign({}, self.state, {
                        isJobAccepted: true
                    })
                    self.setState(newState);
                });
        }
    }

    addMarkerForJob = (jobArgs: any): void => {

        console.log("addMarkerForJob");
        console.log(this.state);

        if (this.state.isJobAccepted || jobArgs.isAssigned) {
            this.processAcceptedMarkers(jobArgs);
        }
        else {
            this.processNotAcceptedMarkers(jobArgs);
        }
    }

    processAcceptedMarkers = (jobArgs: any): void => {

        if (jobArgs.jobUUID != undefined && jobArgs.jobUUID != '')
            this._currentJobUUID = jobArgs.jobUUID;

        let isDriver = this._authService.isDriver();
        let jobClientEmail = jobArgs.clientEmail;
        let jobDriverEmail = jobArgs.driverEmail;
        let newMarkersList = this.state.markers;
        let newPositionForUser = null;
        let newPositionForDriver = null;

        console.log("JOB ACCEPTED WE NEED TO ONLY SHOW THE RELEVANT MARKERS + CURRENT USER");
        //1. Should set all the jobs in markers to assigned now
        //2. Should only show the pair that are in the job if current user is one of them 
        //   otherwise just current user
        let allowedNamed = [this._authService.userEmail()];
        if (this._authService.userEmail() == jobArgs.clientEmail ||
            this._authService.userEmail() == jobArgs.driverEmail) {
            allowedNamed = [jobArgs.clientEmail, jobArgs.driverEmail];
        }
        let finalList: Array<PositionMarker> = new Array<PositionMarker>();
        for (var i = 0; i < this.state.markers.length; i++) {
            if (allowedNamed.indexOf(this.state.markers[i].email) >= 0) {
                let theMarker = this.state.markers[i];
                theMarker.jobForMarker.isAssigned = true;
                finalList.push(theMarker);
            }
        }
        newMarkersList = finalList;

        if (this._authService.userEmail() == jobArgs.clientEmail ||
            this._authService.userEmail() == jobArgs.driverEmail) {

            let clientMarker = _.find(newMarkersList, { 'email': jobArgs.clientEmail });
            if (clientMarker != undefined && clientMarker != null) {
                newPositionForUser = jobArgs.clientPosition;
                clientMarker.position = jobArgs.clientPosition;
            }

            let driverMarker = _.find(newMarkersList, { 'email': jobArgs.driverEmail });
            if (driverMarker != undefined && driverMarker != null) {
                newPositionForUser = jobArgs.driverPosition;
                driverMarker.position = jobArgs.driverPosition;
            }
        }
        else {
            let matchedMarker = _.find(newMarkersList, { 'email': this._authService.userEmail() });
            newPositionForUser = matchedMarker.position;
        }

        //update the state
        this.addClientDetailsToDrivers(newMarkersList);
        var newState = this.updateStateForAcceptedMarker(newMarkersList, newPositionForUser);
        this.updateStateForMarkers(newState, newMarkersList, newPositionForUser, jobArgs);
    }


    processNotAcceptedMarkers = (jobArgs: any): void => {

        if (jobArgs.jobUUID != undefined && jobArgs.jobUUID != '')
            this._currentJobUUID = jobArgs.jobUUID;

        let isDriver = this._authService.isDriver();
        let jobClientEmail = jobArgs.clientEmail;
        let jobDriverEmail = jobArgs.driverEmail;
        let newMarkersList = this.state.markers;
        let newPositionForUser = null;
        let newPositionForDriver = null;

        console.log("JOB NOT ACCEPTED WE NEED TO ONLY ALL");

        //see if the client is in the list (which it may not be). 
        //If it is not, add it, otherwise update it
        if (jobArgs.clientPosition != undefined && jobArgs.clientPosition != null) {
            newPositionForUser = new Position
               (jobArgs.clientPosition.latitude, jobArgs.clientPosition.longitude);
        }

        if (jobClientEmail != undefined && jobClientEmail != null &&
            newPositionForUser != undefined && newPositionForUser != null) {
            let matchedMarker = _.find(this.state.markers, { 'email': jobClientEmail });
            if (matchedMarker == null) {
                newMarkersList.push(new PositionMarker(
                    jobArgs.clientFullName,
                    newPositionForUser,
                    jobArgs.clientFullName,
                    jobArgs.clientEmail,
                    false,
                    isDriver,
                    jobArgs)
                );
            }
            else {
                if (jobArgs.clientPosition != undefined && jobArgs.clientPosition != null) {
                    this.updateMatchedUserMarker(
                        jobClientEmail,
                        newMarkersList,
                        newPositionForUser,
                        jobArgs);
                }
            }
        }

        //see if the driver is in the list (which it may not be). 
        //If it is not, add it, otherwise update it.
        if (jobArgs.driverPosition != undefined && jobArgs.driverPosition != null) {
            newPositionForDriver = 
               new Position(jobArgs.driverPosition.latitude, jobArgs.driverPosition.longitude);
        }

        if (jobDriverEmail != undefined && jobDriverEmail != null &&
            newPositionForDriver != undefined && newPositionForDriver != null) {
            let matchedMarker = _.find(this.state.markers, { 'email': jobDriverEmail });
            if (matchedMarker == null) {
                newMarkersList.push(new PositionMarker(
                    jobArgs.driverFullName,
                    newPositionForDriver,
                    jobArgs.driverFullName,
                    jobArgs.driverEmail,
                    true,
                    isDriver,
                    jobArgs));
            }
            else {
                this.updateMatchedUserMarker(
                    jobDriverEmail,
                    newMarkersList,
                    newPositionForDriver,
                    jobArgs);
            }
        }

        if (isDriver) {
            newPositionForUser = newPositionForDriver;
        }

        //update the state
        this.addClientDetailsToDrivers(newMarkersList);
        var newState = this.updateStateForNewMarker(newMarkersList, newPositionForUser);
        this.updateStateForMarkers(newState, newMarkersList, newPositionForUser, jobArgs);
    }

    addClientDetailsToDrivers = (newMarkersList: PositionMarker[]): void => {
        let clientMarker = _.find(newMarkersList, { 'isDriverIcon': false });
        if (clientMarker != undefined && clientMarker != null) {
            let driverMarkers = _.filter(newMarkersList, { 'isDriverIcon': true });
            for (var i = 0; i < driverMarkers.length; i++) {
                let driversJob = driverMarkers[i].jobForMarker;
                driversJob.jobUUID = clientMarker.jobForMarker.jobUUID;
                driversJob.clientFullName = clientMarker.jobForMarker.clientFullName;
                driversJob.clientEmail = clientMarker.jobForMarker.clientEmail;
                driversJob.clientPosition = clientMarker.jobForMarker.clientPosition;
            }
        }
    }

    updateStateForMarkers = (newState: any, newMarkersList: PositionMarker[], 
                             newPositionForUser: Position, jobArgs:any): void => {

        //Update the list of position markers in the PositionService
        this._positionService.clearUserJobPositions();
        this._positionService.storeUserJobPositions(newMarkersList);

        //Update the position in the PositionService
        if (newPositionForUser != undefined && newPositionForUser != null) {
            this._positionService.clearUserPosition();
            this._positionService.storeUserPosition(newPositionForUser);
        }

        this._jobService.clearUserIssuedJob();
        this._jobService.storeUserIssuedJob(jobArgs);

        //update the state
        this.setState(newState);
    }

    updateMatchedUserMarker = (jobEmailToCheck: string, newMarkersList: PositionMarker[],
        jobPosition: Position, jobForMarker:any): void => {

        if (jobEmailToCheck != undefined && jobEmailToCheck != null) {

            let matchedMarker = _.find(this.state.markers, { 'email': jobEmailToCheck });
            if (matchedMarker != null) {
                //update its position
                matchedMarker.position = jobPosition;
                matchedMarker.jobForMarker = jobForMarker;
            }
        }
    }

    updateStateForNewMarker = (newMarkersList:PositionMarker[], position: Position): any => {

        if (position != null) {
            return Object.assign({}, this.state, {
                currentPosition: position,
                markers: newMarkersList
            })
        }
        else {
           return Object.assign({}, this.state, {
                markers: newMarkersList
            })
        }
    }

    updateStateForAcceptedMarker = (newMarkersList: PositionMarker[], position: Position): any => {

        if (position != null) {
            return Object.assign({}, this.state, {
                currentPosition: position,
                markers: newMarkersList,
                isJobAccepted: true
            })
        }
        else {
            return Object.assign({}, this.state, {
                markers: newMarkersList,
                isJobAccepted: true
            })
        }
    }

    shouldShowMarkerForJob = (jobArgs: any): boolean => {

        let isDriver = this._authService.isDriver();
        let currentJob = this._jobService.currentJob();
        let hasJob = currentJob != undefined && currentJob != null;

        //case 1 - No job exists, to allow driver to add their mark initially
        if (!hasJob && isDriver)
            return true;
        
        //case 2 - Job exists and is unassigned and if there is no other active 
        //         job for this client/ driver
        if (hasJob && !currentJob.isAssigned)
            return true;

        //case 3 - If the job isAssigned and its for the current logged in client/driver
        if (hasJob && currentJob.isAssigned) {
            if (currentJob.clientEmail == jobArgs.clientEmail) {
                return true;
            }
            if (currentJob.driverEmail == jobArgs.driverEmail) {
                return true;
            }
        }
        return false;
    }

    pushOutJob = (newPosition: Position, jobForMarker : any): void => {
        var self = this;
        let currentUser = this._authService.user();
        let isDriver = this._authService.isDriver();
        let hasIssuedJob = this._jobService.hasIssuedJob();
        let currentJob = jobForMarker;
        let currentPosition = this._positionService.currentPosition();
        var localClientFullName = '';
        var localClientEmail = '';
        var localClientPosition = null;
        var localDriverFullName = '';
        var localDriverEmail = '';
        var localDriverPosition = null;
        var localIsAssigned = false;

        if (hasIssuedJob) {
            if (currentJob.isAssigned != undefined && currentJob.isAssigned != null) {
                localIsAssigned = currentJob.isAssigned;
            }
            else {
                localIsAssigned = false;
            }
        }

        //clientFullName
        if (hasIssuedJob) {
            if (currentJob.clientFullName != undefined && currentJob.clientFullName != "") {
                localClientFullName = currentJob.clientFullName;
            }
            else {
                localClientFullName = !isDriver ? currentUser.fullName : '';
            }
        }
        //clientEmail
        if (hasIssuedJob) {
            if (currentJob.clientEmail != undefined && currentJob.clientEmail != "") {
                localClientEmail = currentJob.clientEmail;
            }
            else {
                localClientEmail = !isDriver ? currentUser.email : '';
            }
        }
        //clientPosition
        if (hasIssuedJob) {
            if (!isDriver) {
                localClientPosition = newPosition
            }
            else {
                if (currentJob.clientPosition != undefined && currentJob.clientPosition != null) {
                    localClientPosition = currentJob.clientPosition;
                }
            }
        }

        if (hasIssuedJob) {
            //driverFullName
            if (currentJob.driverFullName != undefined && currentJob.driverFullName != "") {
                localDriverFullName = currentJob.driverFullName;
            }
            else {
                localDriverFullName = isDriver ? currentUser.fullName : '';
            }
            //driverEmail
            if (currentJob.driverEmail != undefined && currentJob.driverEmail != "") {
                localDriverEmail = currentJob.driverEmail;
            }
            else {
                localDriverEmail = isDriver ? currentUser.email : '';
            }

            //driverPosition
            if (isDriver) {
                localDriverPosition = newPosition
            }
            else {
                if(currentJob.driverPosition != undefined && currentJob.driverPosition != null) {
                    localDriverPosition = currentJob.driverPosition;
                }
            }
        }
        else {
            localDriverFullName = currentUser.fullName;
            localDriverEmail = currentUser.email;
            localDriverPosition = isDriver ? currentPosition : null;
        }

        var newJob = {
            jobUUID: this._currentJobUUID != undefined && this._currentJobUUID != '' ?
                this._currentJobUUID : '',
            clientFullName: localClientFullName,
            clientEmail: localClientEmail,
            clientPosition: localClientPosition,
            driverFullName: localDriverFullName,
            driverEmail: localDriverEmail,
            driverPosition: localDriverPosition,
            vehicleDescription: isDriver ?
                this._authService.user().vehicleDescription : '',
            vehicleRegistrationNumber: isDriver ?
                this._authService.user().vehicleRegistrationNumber : '',
            isAssigned: localIsAssigned,
            isCompleted: false
        }

        console.log("handlpushOutJob job");
        console.log(newJob);
        this.makePOSTRequest('job/submit', newJob, self,
            function (jdata, textStatus, jqXHR) {
                self._jobService.clearUserIssuedJob();
                self._jobService.storeUserIssuedJob(newJob);
            });
    }

    createDriverMarker = (
        driver: any,
        event: any): PositionMarker => {

        let localDriverFullName = driver.fullName;
        let localDriverEmail = driver.email;
        let localDriverPosition = new Position(event.latLng.lat(), event.latLng.lng());
        let localVehicleDescription = this._authService.user().vehicleDescription;
        let localVehicleRegistrationNumber = this._authService.user().vehicleRegistrationNumber;
        let currentUserIsDriver = this._authService.isDriver();

        var driverJob = {
            jobUUID: this._currentJobUUID != undefined && this._currentJobUUID != '' ?
                this._currentJobUUID : '',

            driverFullName: localDriverFullName,
            driverEmail: localDriverEmail,
            driverPosition: localDriverPosition,
            vehicleDescription: localVehicleDescription,
            vehicleRegistrationNumber: localVehicleRegistrationNumber,
            isAssigned: false,
            isCompleted: false
        }		

        return new PositionMarker(
            localDriverFullName,
            localDriverPosition,
            localDriverFullName,
            localDriverEmail,
            true,
            currentUserIsDriver,
            driverJob
        );
    }
    
    ratingsDialogOkCallBack = (theRatingScore: number) => {
        console.log('RATINGS OK CLICKED');

        var self = this;
        let currentUser = this._authService.user();
        let isDriver = this._authService.isDriver();
        let currentJob = this._jobService.currentJob();
        var ratingJSON = null;

        if (!isDriver) {
            ratingJSON = {
                fromEmail: this._authService.userEmail(),
                toEmail: currentJob.driverEmail,
                score: theRatingScore
            }
        }
        else {
            ratingJSON = {
                fromEmail: this._authService.userEmail(),
                toEmail: currentJob.clientEmail,
                score: theRatingScore
            }
        }

        this.makePOSTRequest('rating/submit/new', ratingJSON, self,
            function (jdata, textStatus, jqXHR) {
                this._jobService.clearUserIssuedJob();
                this._positionService.clearUserJobPositions();
                this.setState(
                    {
                        okDialogHeaderText: 'Ratings',
                        okDialogBodyText: 'Rating successfully recorded',
                        okDialogOpen: true,
                        okDialogKey: Math.random(),
                        markers: new Array<PositionMarker>(),
                        currentPosition: null,
                        isJobAccepted: false,
                        finalActionHasBeenClicked: true
                    });
            });
    }
   
    makePOSTRequest = (route: string, jsonData: any, context: ViewJob, 
                      doneCallback: DoneCallback) => {
        $.ajax({
            type: 'POST',
            url: route,
            data: JSON.stringify(jsonData),
            contentType: "application/json; charset=utf-8",
            dataType: 'json'
        })
        .done(function (jdata, textStatus, jqXHR) {
            doneCallback(jdata, textStatus, jqXHR);
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            const newState = Object.assign({}, context.state, {
                okDialogHeaderText: 'Error',
                okDialogBodyText: jqXHR.responseText,
                okDialogOpen: true,
                okDialogKey: Math.random()
            })
            context.setState(newState)
        });
    }

    jobCancelledCallBack = () => {
        console.log('CANCEL YES CLICKED');
        this._jobService.clearUserIssuedJob();
        this._positionService.clearUserJobPositions();
        this.setState(
            {
                okDialogHeaderText: 'Job Cancellaton',
                okDialogBodyText: 'Job successfully cancelled',
                okDialogOpen: true,
                okDialogKey: Math.random(),
                markers: new Array<PositionMarker>(),
                currentPosition: null,
                isJobAccepted: false,
                finalActionHasBeenClicked: true
            });
    }

    jobNotCancelledCallBack = () => {
        console.log('CANCEL NO CLICKED');
        this.setState(
            {
                okDialogHeaderText: 'Job Cancellaton',
                okDialogBodyText: 'Job remains open',
                okDialogOpen: true,
                okDialogKey: Math.random(),
                finalActionHasBeenClicked: true
            });
    }

    okDialogCallBack = () => {
        console.log('OK on OkDialog CLICKED');
        this.setState(
            {
                okDialogOpen: false
            });
    }
}

Some Highlights

  • We use RX.Js to listen to new events straight over the Comet based forever frame, that the server side Play scala code pushes a message out on
  • There was a funny thing with driver acceptance which I originally wanted to be a button on a drivers marker within the map. However, this caused an issue with the Map where it would get a Map event when clicking on an overlay (higher Z-Order so should not happen). This is a feature of the React Google Map component. I could not find a fix I liked (I did mess around with form event mouseEnter/mouseLeave but it was just not that great, so I opted to choose to put the acceptance of driver outside of the map, thus avoiding the issue altogether)

If you are curious how to run and see what you need to install, please see the README.MD in the codebase that has full instructions.

What’s It Looks Like When It's Run?

Some scenarios of what it looks like running are shown below.

In order to run it to this point, I normally follow this set of steps afterwards:

  • Open a tab, login as a passenger that I had created
  • Go to the “create job” page, click the map, push the “create job” button
  • Open a NEW tab, login as a new driver, go to the “view job” page
  • On the 1st tab (passenger), click the map to push passenger position to driver
  • On the 2nd tab (driver), click the map to push driver position to passenger
  • Repeat last 4 steps for additional driver
  • On client tab, pick driver to accept, click accept button
  • Complete the job from client tab, give driver rating
  • Complete the job from paired driver tab, give passenger rating
  • Go to “view rating” page, should see ratings

One of the challenges with an app like this, is that it is a streaming app. So this means that when a client pushes out a new job, there may be no-one listening for that job. Drivers may not even be logged in at all, or may login later, so they effectively subscribe late. So for this app dealing with that was kind of out of scope. So to remedy this, you need to ensure that position updates (clicking on the map for the given user browser session (i.e., tab)) gets pushed to other user browser sessions where the marker is not currently shown.

In a real world app, we might choose to do one of the following to fix this permanently:

  • Store some, and when a new user joins, grab all unassigned passengers/driver within some geographical area
  • Store last N-Many passenger/driver positions and push these on new login (there is no guarantee that these are in the same geographical area as us though, could be completely unrelated/of no practical concern for the current user)

Anyway, as I say this is out of scope for this demo project, but I hope that it does give you some insight as to why you need to push position updates manually.

This gives you an example of what it all looks like when it's running (not accepted yet):

This is what it looks like for the following setup:

1 x passenger (sacha barber)

2 x driver (driver 1 / driver 2)

Passenger (sacha) sees this:

image

Driver 1 sees this:

image

Driver 2 sees this:

image

So now, let's see what happens when we accept one of the drivers. I have chosen “driver 1” for this example.

This gives you an example of what it all looks like when its running (after job accepted between passenger/driver1).

Here is what things look like after job has been accepted.

Passenger (sacha) sees this:

See how now only the passenger (sacha) and the driver chosen (driver 1) are now shown:

image

Driver 1 sees this:

See how now only the passenger (sacha) and the driver chosen (driver 1) are now shown:

image

Driver 2 sees this:

Since this driver (driver 2) was not chosen, this driver's session now only shows itself:

image

Conclusion / Errors Made Along The Way

As with any reasonable size project (and this definitely is in that category for me), mistakes will be made. And whilst I am happy that I got all of this to work, I have certainly made a few mistakes, such as:

  • There should have been 2 streams of jobs, client and driver.
  • There should have been no separation between creating a job and viewing a job.
  • There was a funny thing with driver acceptance which I originally wanted to be a button on a driver's marker within the map. However, this caused an issue with the Map where it would get a Map event when clicking on an overlay (higher Z-Order so should not happen). This is a feature of the React Google Map component. I could not find a fix I liked (I did mess around with form event mouseEnter/mouseLeave but it was just not that great, so I opted to choose to put the acceptance of driver outside of the map, thus avoiding the issue altogether).
  • There is currently a bug when a job becomes paired between a passenger/driver, and new position updates are not reflected. This is probably a couple of line changes, but at this stage I’m like MEH. I am kind of done, I proved what I want to try, and the main points I set out to prove all work without this, so Math.Power(MEH, Infinity).
  • When a driver joins (possibly later after other drivers), they should instantly know about other drivers/client. Right now, the users whose position you are missing would need to push a position update to get that user reflected on the users screen who is missing that user. I could have done something where every X milliseconds ALL current user positions are pushed to all other users, but this would clearly not scale. Another alternative would have been to store the state of all uses positions in a database every time that the map was clicked, and then when a new user joins find all users/jobs in the general area providing they are not already assigned to a job. Being completely honest, this was not really what this project was about either. The main cut and thrust of this article was to mess around with Kafka Streams and see how they work, and how I could stream stuff on top of that all the way back to a browser session. To this end, the project has been a massive success.

On a personal note, I have had a great time writing this project and have learnt loads doing it. For example, I am now way more familiar with how Akka Streams work, and I now know Play and Webpack to a fairly good level. I thoroughly recommend ALL of you pick your own mad cap projects and roll with them.

For me, next, I am either going to be dipping back into Azure stuff, or more scala where we will learn cats and Shapeless.

Image 7 Image 8

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
PraiseLong time no hear Pin
Shaun Stewart28-Nov-17 2:03
Shaun Stewart28-Nov-17 2:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.