AppStateESS/homestead

View on GitHub
javascript/CheckOut/CheckOut.jsx

Summary

Maintainability
F
4 days
Test Coverage
import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import PropTypes from 'prop-types';

class CheckOut extends React.Component{
    constructor(props){
        super(props);
        // damage_types, existing_damage and residents are plugged in by the CheckOut.html template
        this.state = {
            previousKeyCode: this.props.previous_key_code,
            keyReturned: null,
            keyCode : null,
            existingDamage: this.props.existing_damage,
            newDamage: [],
            residents: this.props.residents,
            damageTypes: this.props.damage_types,
            properCheckout : null,
            improperNote : '',
            checkinId: this.props.checkin_id,
            bannerId: this.props.banner_id
        };
        this.updateKeyReturned = this.updateKeyReturned.bind(this);
        this.updateKeyCode = this.updateKeyCode.bind(this);
        this.updateNewDamage = this.updateNewDamage.bind(this);
        this.updateProperCheckout = this.updateProperCheckout.bind(this);
        this.updateImproperNote = this.updateImproperNote.bind(this);
        this.isReadyToPost = this.isReadyToPost.bind(this);
        this.postCheckOut = this.postCheckOut.bind(this);
    }
    updateKeyReturned(key) {
        this.setState({keyReturned: key});
    }
    updateKeyCode(code){
        this.setState({keyCode : code});
    }
    updateNewDamage(damage){
        this.setState({newDamage : damage});
    }
    updateProperCheckout(value){
        var checkout = Number(value);
        if (checkout === 0) {
            this.setState({
                properCheckout : checkout,
                improperNote : ''
            });
        } else {
            this.setState({properCheckout : checkout});
        }
    }
    updateImproperNote(note){
        this.setState({improperNote : note.target.value});
    }
    isReadyToPost(){
        if (this.state.keyReturned === null) {
            return false;
        }
        if (this.state.keyReturned && this.state.keyCode === null) {
            return false;
        }
        if (this.state.properCheckout === null) {
            return false;
        }
        if (this.state.properCheckout === 0 && this.state.improperNote.length === 0) {
            return false;
        }

        return true;
    }
    postCheckOut() {
        var forward_url = 'index.php?module=hms&action=ShowCheckoutDocument&checkinId=' + this.state.checkinId;
        var damages = this.state.newDamage;

        // if damage array has a form at the end, pop it off
        if (damages.length > 0 && damages[damages.length - 1].damage_type === 0) {
            damages.pop();
        }
        $.post('index.php', {
            module: 'hms',
            action: 'CheckoutFormSubmit',
            checkinId  : this.state.checkinId,
            bannerId : this.state.bannerId,
            keyReturned: this.state.keyReturned,
            keyCode: this.state.keyCode,
            newDamage:  damages,
            properCheckout: this.state.properCheckout,
            improperNote: this.state.improperNote
        }).done(function(data) {
            window.location.href = forward_url;
        });
    }
    render() {
        var disable = !this.isReadyToPost();
        return (
            <div>
                <KeyReturn keyReturned={this.state.keyReturned} updateKeyReturned={this.updateKeyReturned} updateKeyCode={this.updateKeyCode} previousKeyCode={this.state.previousKeyCode}/>
                <ExistingDamages responsible={this.state.responsible} existingDamage={this.state.existingDamage} damageTypes={this.state.damageTypes}/>
                <NewRoomDamages damageTypes={this.state.damageTypes} residents={this.state.residents} updateNewDamage={this.updateNewDamage} newDamage={this.state.newDamage}/>
                <CheckOutCompletion updateProperCheckout={this.updateProperCheckout} updateImproperNote={this.updateImproperNote} />
                <hr />
                <p className="text-center"><button disabled={disable} className="btn btn-primary btn-lg" onClick={this.postCheckOut}><i className="fa fa-check"></i> Complete Checkout</button></p>
            </div>
        );
    }
}

class KeyReturn extends React.Component{
    constructor(props){
        super(props);

        this.state = {
            codeAlert: false,
            keyCodeInput : null
        };
        this.keyTurnIn = this.keyTurnIn.bind(this);
        this.updateKeyCode = this.updateKeyCode.bind(this);
        this.checkAlert = this.checkAlert.bind(this);
    }
    keyTurnIn(event) {
        var returned = event.target.value === 'true';
        this.props.updateKeyReturned(returned);
        if (!returned) {
            ReactDOM.findDOMNode(this.refs.keyCode).value = '';
            this.setState({
                keyCodeInput : null,
                codeAlert:false
            });
            this.props.updateKeyCode(null);
        }
    }
    updateKeyCode(keycode) {
        var keyCodeInput = keycode.target.value;
        this.setState({
            keyCodeInput : keyCodeInput
        });

        this.props.updateKeyCode(keyCodeInput);
        this.checkAlert(keyCodeInput);
    }
    checkAlert(keyCodeInput) {
        if (keyCodeInput === null || keyCodeInput.length === 0 || String(keyCodeInput) === String(this.props.previousKeyCode)) {
            this.setState({
                codeAlert : false
            });
        } else {
            this.setState({
                codeAlert : true
            });
        }
    }
    render() {
        var codeAlertDiv = <div></div>;
        if (this.state.codeAlert) {
            codeAlertDiv = <div className="alert alert-warning"><strong>Note:</strong> This keycode does not match the checkin key code: {this.props.previousKeyCode}</div>;
        }

        return (
            <div>
                <h3>Key status</h3>
                <div className="row">
                    <div className="col-sm-3">
                        <div>
                            <label>
                                <input name="keyReturned" onChange={this.keyTurnIn} type="radio" value='true'/>{' '} Key returned
                            </label>
                        </div>
                        <div>
                            <label>
                                <input name="keyReturned" onChange={this.keyTurnIn} type="radio" value='false'/>{' '} Key not returned
                            </label>
                        </div>
                    </div>
                    <div className="col-sm-3">
                        <input className="form-control" disabled={!this.props.keyReturned} id="keyCode" name="keyCode" placeholder="Enter key code" onBlur={this.updateKeyCode} type="text" ref="keyCode"/>
                    </div>
                    <div className="col-sm-6">
                        {codeAlertDiv}
                    </div>
                </div>
            </div>
        );
    }
}

class ExistingDamages extends React.Component{
    render() {
        if (this.props.existingDamage.length === 0) {
            return (
                <div>
                    <h3>Existing Room Damage</h3>
                    <p><em>No previous damage recorded.</em></p>
                </div>
            );
        }
        return (
            <div>
                <h3>Existing Room Damage</h3>
                {this.props.existingDamage.map(function(value, key){ return (
                <Damage category={this.props.damageTypes[value.damage_type].category} description={this.props.damageTypes[value.damage_type].description}
                    key={key} note={value.note} reportedOn={value.reported_on} side={value.side} residents={value.residents}/>
                ); }, this)}
            </div>
        );
    }
}

class NewRoomDamages extends React.Component{
    constructor(props){
        super(props);

        this.state = {formActive: false};
        this.addDamageForm = this.addDamageForm.bind(this);
        this.removeForm = this.removeForm.bind(this);
        this.pushDamage = this.pushDamage.bind(this);
    }
    addDamageForm() {
        var formObj = {};
        formObj.form = true;
        formObj.reportedOn = 0;
        formObj.side = '';
        formObj.damage_type = 0;
        formObj.category = '';
        formObj.description = '';
        formObj.note = '';
        formObj.residents = [];
        this.pushDamage(formObj);
    }
    removeForm() {
        var updatedDamages = this.props.newDamage;
        updatedDamages.pop();
        this.props.updateNewDamage(updatedDamages);
        this.setState({
            formActive: false
        });
    }
    pushDamage(damage) {
        var updatedDamages = this.props.newDamage;
        updatedDamages.push(damage);
        this.props.updateNewDamage(updatedDamages);
        this.setState({
            formActive: damage.form
        });
    }
    render() {
        var button = this.state.formActive ? null : <button className="btn btn-success" onClick={this.addDamageForm} autoFocus={true}>
                <i className="fa fa-plus"></i>{' '}Add damage</button>;
        return (
            <div>
                <h3>New Room Damages</h3>
                {this.props.newDamage.map(function(value, key){ if (value.form === true) { return (
                <DamageForm {...this.props} key={key} pushDamage={this.pushDamage} removeForm={this.removeForm}/>
                ); } else { return (
                <Damage category={value.category} description={value.description} key={key} note={value.note} reportedOn={value.reportedOn} side={value.side} residents={value.residents}/>
                ); } }, this)} {button}
            </div>
        );
    }
}

class DamageForm extends React.Component{
    constructor(props){
        super(props);
        var resTemp = [];
        this.props.residents.map(function(value, i){
            resTemp.push({studentId:value.studentId, selected:false});
        });
        this.state = {
            damage_type: null,
            side: null,
            note: null,
            residents: resTemp,
            error: []
        }
        this.errorFree = this.errorFree.bind(this);
        this.shouldComponentUpdate = this.shouldComponentUpdate.bind(this);
        this.categorySelected = this.categorySelected.bind(this);
        this.sideSelected = this.sideSelected.bind(this);
        this.residentSelected = this.residentSelected.bind(this);
        this.updateNote = this.updateNote.bind(this);
        this.saveDamage = this.saveDamage.bind(this);

    }
    errorFree() {
        var all_clear = true;
        var errors = [];
        let resident_selected = false;

        if (this.state.damage_type === null || this.state.damage_type.length === 0) {
            errors.push('category');
            all_clear = false;
        } else {
            var catError = $.inArray('category', errors);
            if (catError !== -1) {
                errors.splice(catError, 1);
            }
        }

        resident_selected = this.state.residents.some(function(value,key){
             return value.selected;
        });

        if (!resident_selected) {
            all_clear = false;
            errors.push('resident');
        } else {
            var resError = $.inArray('resident', errors);
            if (resError !== -1) {
                errors.splice(resError, 1);
            }
        }

        if (this.state.side === null || this.state.side.length === 0) {
            errors.push('side');
            all_clear = false;
        } else {
            var sideError = $.inArray('side', errors);
            if (sideError !== -1) {
                errors.splice(sideError, 1);
            }
        }

        if (this.state.note === null || this.state.note.length === 0) {
            errors.push('note');
            all_clear = false;
        } else {
            var noteError = $.inArray('note', errors);
            if (noteError !== -1) {
                errors.splice(noteError, 1);
            }
        }

        this.setState({error : errors, render: true});
        return all_clear;
    }
    shouldComponentUpdate(nextProps, nextState) {
        if (nextState.render !== undefined) {
            return nextState.render;
        } else {
            return true;
        }
    }
// category is saved as damage_type, which is the actual id, not the
// category/description that is displayed
    categorySelected(selected) {
        this.setState({render: false, damage_type: selected.target.value});
    }
    sideSelected(selected) {
        this.setState({render: false, side: selected.target.value});
    }
    residentSelected(selected) {
        var updatedResidents = this.state.residents;

        // Loop over the list of residents to find the resident corresponding to the checkbox that was just changed
        // Update that resident's selected status according to the new status of the checkbox
        for (var i=0; i < updatedResidents.length; i++){
            // NB: studentId is an int, selected.target.value is a string. Must use an implicit type cast ( + '')
            if((updatedResidents[i].studentId + '') === selected.target.value){
                updatedResidents[i].selected = selected.target.checked;
            }
        }

        console.log(updatedResidents);

        this.setState({render: false, residents: updatedResidents});
    }
    updateNote(note) {
        this.setState({render: false, note: note.target.value});
    }
    saveDamage() {
        if (this.errorFree()) {
            var damage = {};
            damage.form = false;
            damage.reportedOn = Math.floor(Date.now() / 1000);
            damage.side = this.state.side;
            damage.damage_type = this.state.damage_type;
            damage.category = this.props.damageTypes[damage.damage_type].category;
            damage.description = this.props.damageTypes[damage.damage_type].description;
            damage.note = this.state.note;
            damage.residents = [];
            this.state.residents.map(function(val, key){
                if (val.selected) {
                    this.props.residents.map(function(subval, i){
                        if(subval.studentId === val.studentId) {
                            val.name = subval.name;
                            damage.residents.push(val);
                        }
                    });
                }
            }, this);
            this.props.removeForm();
            this.props.pushDamage(damage);
        }
    }
    render() {
        var alert = null;
        var residentClass = 'col-sm-3 ' + ($.inArray('resident', this.state.error) !== -1 ? 'checkout-error-border' : null);
        var noteClass = 'form-control ' + ($.inArray('note', this.state.error) !== -1 ? 'checkout-error-border' : null);
        if (this.state.error.length) {
            alert = <div className="alert alert-danger">Please complete all highlighted fields.</div>;
        }

        return (
            <div className="panel panel-default" id="newDamageForm">
                <div className="panel-body">
                    <div className="row">
                        <div className={residentClass}>
                            <h4>Responsible</h4>
                            {this.props.residents.map(function(val, key){return (
                            <ResidentCheckbox handleChange={this.residentSelected} key={key} name={val.name} value={val.studentId} autoFocus={key === 0}/>
                            ); }, this)}
                        </div>
                        <div className="col-sm-9">
                            <h4>Details</h4>
                            <div className="row">
                                <div className='col-sm-3'>
                                    <SideSelect onChange={this.sideSelected} error={this.state.error} />
                                </div>
                                <div className='col-sm-9'>
                                    <DamageTypeSelect damageTypes={this.props.damageTypes} onChange={this.categorySelected} error={this.state.error}/>
                                </div>
                            </div>
                            <div className="row" style={{marginTop: '1em'}}>
                                <div className="col-sm-12">
                                    <input className={noteClass} maxLength="200" name="note" onChange={this.updateNote} placeholder="Brief description of damage..." type="text"/>
                                </div>
                            </div>
                        </div>
                    </div>
                    <hr/>
                    <div className="text-center">
                        {alert}
                        <button className="btn btn-primary" onClick={this.saveDamage}>
                            <i className="far fa-save"></i>{' '}Save damages
                        </button>{' '}
                        <button className="btn btn-danger-hover" onClick={this.props.removeForm}>
                            <i className="fa fa-times"></i>{' '}Remove
                        </button>
                    </div>
                </div>
            </div>
        );
    }
}

DamageForm.propTypes = {
    // contains _.name, _.studentId
    residents: PropTypes.array
}

class ResidentCheckbox extends React.Component{
    render() {
        return (
            <div className="checkbox">
                <label>
                    <input onChange={this.props.handleChange} type="checkbox" value={this.props.value} autoFocus={this.props.autoFocus}/>{' '}{this.props.name}
                </label>
            </div>
        );
    }
}

class DamageTypeSelect extends React.Component{
    constructor(props){
        super(props);

        this.buildDamageOptions = this.buildDamageOptions.bind(this);

        var damageOptions = this.buildDamageOptions(this.props.damageTypes);
        this.state = {damageOptions: damageOptions};
    }
    buildDamageOptions(damageTypes) {
        var damageOptions = {};
        Object.keys(damageTypes).map(function(key, idx){
            var opt = {};
            var value = damageTypes[key];
            opt.description = value.description;
            opt.id = value.id;
            if (damageOptions[value.category] === undefined) {
                damageOptions[value.category] = [];
            }
            damageOptions[value.category].push(opt);
        });
        return damageOptions;
    }
    render() {
        var damageClass = 'form-control ' + ($.inArray('category', this.props.error) !== -1 ? 'checkout-error-border' : null);
        return (
            <select className={damageClass} onChange={this.props.onChange}>
                <option></option>
                {Object.keys(this.state.damageOptions).map(function(category, idx){ return(
                <optgroup key={idx} label={category}>
                    {this.state.damageOptions[category].map(function(optValue, key){ return (
                    <option key={key} value={optValue.id}>{optValue.description}</option>
                    ); })}
                </optgroup>
                ); }, this)}
            </select>
        );
    }
}

class SideSelect extends React.Component{
    render() {
        var sideClass = 'form-control damage-select ' + ($.inArray('side', this.props.error) !== -1 ? 'checkout-error-border' : null);
        return (
            <select className={sideClass} onChange={this.props.onChange}>
                <option></option>
                <option value="Left">Left side</option>
                <option value="Right">Right side</option>
                <option value="Both">Both sides</option>
            </select>
        );
    }
}

class Damage extends React.Component{
    render() {
        var residentList = '';
        if (this.props.residents !== undefined) {
            this.props.residents.map(function(val){
                residentList = residentList.length ? residentList + ', ' + val.name : val.name;
            });
        }
        var reportedOn = new Date();
        var month = 1;
        reportedOn.setTime(this.props.reportedOn * 1000);
        month = reportedOn.getMonth() + 1;
        return (
            <div className="panel panel-danger">
                <div className="panel-heading">
                    <div className="row">
                        <div className="col-sm-6"><strong>Damage:</strong> {this.props.category} - {this.props.description}</div>
                        <div className="col-sm-2"><strong>Side:</strong> {this.props.side}</div>
                        <div className="col-sm-4"><strong>Reported on:</strong> {reportedOn.getFullYear() + '/' + month + '/' + reportedOn.getDate()}</div>
                    </div>
                </div>
                <div className="panel-body">
                    <p><strong>Description:</strong> {this.props.note}</p>
                    {residentList.length ? <p><strong>Responsibility:</strong> {residentList}</p> : null}
                    {this.props.removable ? <button>Remove</button> : null}
                </div>
            </div>
        );
    }
}

class CheckOutCompletion extends React.Component{
    constructor(props){
        super(props);

        this.state = {noteDisabled : true};
        this.handleChange = this.handleChange.bind(this);
    }
    handleChange(event) {
        this.props.updateProperCheckout(event.target.value);
        if (event.target.value === '0') {
            this.setState({
                noteDisabled : false
            });
        } else {
            ReactDOM.findDOMNode(this.refs.checkoutNotes).value = '';
            this.setState({
                noteDisabled : true
            });
        }
    }
    render() {
        return (
            <div>
                <h3>Final Checkout</h3>
                <div className="radio">
                    <label>
                        <input onChange={this.handleChange} type="radio" name="properCheckout" value="1"/>{' '}Proper Checkout
                    </label>
                </div>
                <div className="radio">
                    <label>
                        <input onChange={this.handleChange} type="radio"  name="properCheckout" value="0"/>{' '}Improper Checkout
                    </label>
                </div>
                <textarea ref="checkoutNotes" className="form-control" name="improperCheckoutNote" placeholder="Please explain why this was an improper checkout" onChange={this.props.updateImproperNote} disabled={this.state.noteDisabled} />
            </div>
        );
    }
}

// This script will not run after compiled UNLESS the below is wrapped in $(window).load(function(){...});
ReactDOM.render(<CheckOut residents={window.residents} existing_damage={window.existing_damage} previous_key_code={window.previous_key_code}
damage_types={window.damage_types} checkin_id={window.checkin_id} banner_id={window.banner_id}/>, document.getElementById('checkout'));