How to Build a Venue Booking App with SashiDo - Part 2

Nishka Kotian

Hello again! This post is a continuation to the first part where I've explained about user authentication and storing venue details for my Venue Booking App. Today, we will cover querying the database and also look into how the booking functionality can be implemented.
In case you aren't familiar with ES6 arrow functions and template literals I would suggest reading about them first. I have added useful links at the bottom of the tutorial for your reference.

Table Of Contents

Displaying venues on the dashboard

After the owner has added venues, there has to be a way to get all that information. In the customer's dashboard top 25 venues are shown and in the owner's dashboard under the Venues tab all the venues belonging to that owner are displayed.

Owner's dashboard:
sked_OwnerDashboard

This owner has 3 venues listed.

When an owner first creates an account, they wouldn't have any venues. So in the html I have created two divs one for new owners with no venues and another for those who have at least one venue.

<body onload="getOwnerData();">

...

<!--Div to display for new owners.No venues added yet-->
<div class="card mt-4 w-75 defaultMsg d-none" id="novenues">
    <div class="card-body">
        <h5 class="mb-3" style="color: #fa5757;">Welcome to SKED!</h5>
        Your listed venues will show up here after you add them.<br>
        <button type="button" class="btn mt-3" style="background:#7cebeb;"
            data-bs-toggle="modal" data-bs-target="#addVenueModal">Add a
            venue</button>
    </div>
</div>
<!--Div to display for owners with at least 1 venue added-->
<div id="displayVenues" class="row">
    <h5 class="mt-4 mb-3 d-none" id="yourVenuesHeading">Your Venues</h5>
</div>   

...

</body>               
function getOwnerData() {
    const user = Parse.User.current();
    document.getElementById("ownername").innerHTML = user.attributes.username;

    const Venues = Parse.Object.extend("Venues");
    const query = new Parse.Query(Venues);
    query.equalTo("owner", user);
    query.find().then(function findVenues(results) {
        if (results.length == 0) {
            document.getElementById("novenues").classList.remove("d-none");
        } else {
            document.getElementById("yourVenuesHeading").classList.remove("d-none");
            const displayArea = document.getElementById("displayVenues");
            results.forEach((venue, index) => {
                if (i == 11) { i = 0; }
                displayVenue(displayArea, venue);
                i += 1;
            });
        }

        /* Insert code to fetch booking details here */

    }, function error(err) {
        console.log('Error : ', err);
    });
}

To fetch the data, you have to find out the owner who is logged in using Parse.User.current() and then create a new Parse.Query which can be used for querying objects from the class. You can specify conditions based on which the query will return matching records. query.equalTo("owner", user); means that I want to get all Venues which have the owner column set to user. Note that user is a pointer to the instance of the current user in the User class.query.find() retrieves all the rows which satisfy the query and returns a promise.
The displayVenue function is a utility function that takes as parameters the id of the div element inside which the venue cards will be displayed and a Venue object.

var i = 0; //iterator for colours in venue cards
const colours = ["#8a068f", "#06148f", "#c70a62", "#0a9956", "#e78659", "#87b40d", "#0791b4", "#8609ce", "#4c7e80", "#c2427e", "#838080"];

function displayVenue(displayArea, venue) {
    var venuediv = document.createElement("div");
    venuediv.className = "venue col-md-6 col-lg-3 mb-4 d-flex align-items-stretch text-center";
    var photo = venue.get("image1").url();
    var objId = venue.id;

    venuediv.innerHTML =
        `<div class='card' id='${objId}' onclick='venueDetails(this)' style ='border-bottom: 4px solid ${colours[i]};'>
            <img class='card-img-top' height='230px' src='${photo}'>
            <div class='card-body'>
                <h5 class='card-title'>${venue.get("venueName")}</h5>
                <span class='tag tag-place'><small class="capitalised">${venue.get("city")}</small></span>
            </div>
        </div>`;
    displayArea.appendChild(venuediv);
}

The .get method of an object can be used to obtain the value of any field in it.

Notice that for the cards I'm assigning the id to the id of the venue object. So, when an user clicks on the card, its id would be added as a parameter to the url which would then be used to identify the venue whose details have to be displayed.

function venueDetails(el) {
    window.location.href = "venue.html?id=" + el.id;
}

Customer's dashboard:

sked_customerDashboard

The customer can check venues as well as their bookings by toggling the button in the navbar. In the html I've created empty containers for each of them.

<body onload="showVenues(); getCustomerBookings();">

<nav>
     <button class="navbtns navlink" id="toggle_btn" 
onclick="showOther(this)">Show Bookings</button>
</nav>
...

<div id="showVenuesHomepg" class="row"></div>
<div id="customerBookings" class="row my-4 d-none"></div>
...

</body>
//Toggle between showing venues and bookings
function showOther(el) {
    if (el.innerHTML == "Show Venues") {
        el.innerHTML = "Show Bookings";
        document.getElementById("customerBookings").classList.add("d-none");
        document.getElementById("venues").style.display = "block";
    }
    else {
        el.innerHTML = "Show Venues";
        document.getElementById("venues").style.display = "none";
        document.getElementById("customerBookings").classList.remove("d-none");
    }
}

function showVenues() {
    const Venues = Parse.Object.extend("Venues");
    const query = new Parse.Query(Venues);
    query.limit(25);
    query.find().then(function success(results) {
        results.forEach((venue, index) => {
            const displayArea = document.getElementById("showVenuesHomepg");
            if (i == 11) { i = 0 };
            displayVenue(displayArea, venue);
            i += 1;
        });
    }, function error(err) {
        console.log("Error : ", err);
    });
}

The showVenues function is quite similar to the getOwnerData function except that here I'm fetching the top 25 rows in the Venues class using query.limit(25).By default though, parse returns the top 100 query results.

Filtering venues based on location

As the number of venues gets larger a function that can filter venues based on their location would be useful. Now, we'll see how this can be done.

In the customer.html page create an input field to let the user enter a city name.

<div class="container">
    <h2 id="venHeading" class="my-4 text-center">VENUES</h2>
    <div class="input-group input-group-lg mb-4">
        <input type="text" id="locationfilter" name="venuesfilter" class="form-control" aria-label="searchBar" aria-describedby="locationSearchBar" placeholder="Enter a location..">
        <button type="button" class="btn btn-secondary" id="locationSearchBar"
            onclick="filterVenues();">Search</button>
    </div>
    <div id="filterNoResults" class="my-3 text-center text-danger"></div>
    <div id="showVenuesHomepg" class="row"></div>
</div>

Earlier we made a query based on the owner column. Now, we're interested in querying based on the city column.

function filterVenues() {
    document.getElementById("filterNoResults").innerHTML = "";
    var loc = document.getElementById("locationfilter").value.toLowerCase();
    const Venues = Parse.Object.extend("Venues");
    const query = new Parse.Query(Venues);
    query.equalTo("city", loc);
    query.find().then(function findVenues(results) {
        if (results.length == 0) {
            document.getElementById("filterNoResults").innerHTML = "No venues found !";
        } else {
            const displayArea = document.getElementById("showVenuesHomepg");
            displayArea.textContent = ""; //Remove all venues so as to display only the filtered venues
            results.forEach((venue, index) => {
                if (i == 11) { i = 0; }
                displayVenue(displayArea, venue);
                i += 1;
            });
        }
    }, function error(err) {
        alert('Error : ', err.message);
    });
}

To make this query a little robust, while saving the city for the Venue object I had first converted it to lower case so that now we can convert the input field's value to lowercase and make the search case insensitive.

Display venue details

When clicking on any of the venue card, the insertDetails function is triggered which shows a new page with information about the venue which contains:

  1. Images of the venue.
  2. Details about location, timings, etc.
  3. A calendar. If any date on this calendar is clicked, it shows the time slots that have already been booked so that customers can plan their event accordingly.
  4. A form to send a booking request.

sked_VenueDetailsPage

You can find the code for this page in the venue.html file where I have created empty containers for venue images, details, calendar and added a booking form. Here's a rough outline:

<body onload="insertDetails(); getDates();">
     <div id="loader" class="centered"></div>

    /* Insert navbar */

    <div class="container my-4 whileLoadHide">

        /* slideshow to show images of the venue */

        /* empty divs to show details like timings,address etc */

    </div>

    <div class="container my-4 whileLoadHide" id="calholder">
        
        /* Empty calendar */

    </div>

    <div id="bookVenue" class="container mb-4 whileLoadHide">
        
        /* Form to book venue */

    </div>
</body>

To get details about the venue from the database the function below is used:

var params, venueId,flag;

function insertDetails() {
    params = new URLSearchParams(location.search); //get the parameters in query string
    venueId = params.get('id');
    const Venue = Parse.Object.extend("Venues");
    const query = new Parse.Query(Venue);
    query.get(venueId).then((venue) => {
        // The object was retrieved successfully.
        document.getElementById("brand").innerHTML = venue.get("venueName");
        document.getElementById("img1container").innerHTML = `<img class="d-block w-100" src="${venue.get("image1").url()}" alt="First Image" style="max-height:720px">`
        document.getElementById("img2container").innerHTML = `<img class="d-block w-100" src="${venue.get("image2").url()}" alt="Second Image" style="max-height:720px">`
        document.getElementById("desc").innerHTML = venue.get("description");
        document.getElementById("city").innerHTML = venue.get("city");
        document.getElementById("address").innerHTML = venue.get("address");
        document.getElementById("days").innerHTML = venue.get("daysAvailable");
        document.getElementById("timing").innerHTML = venue.get("timings");

        var hiddencontent = document.getElementsByClassName("whileLoadHide");
        while (hiddencontent.length != 0) {
            hiddencontent[0].classList.remove("whileLoadHide");
        }
        document.getElementById("loader").style.display = "none";

    }, (err) => {
        // The object could not be retrieved.
        alert("Error occured: ", err.message);
        document.getElementById("loader").style.display = "none";

    });
}

The query.get method can be used to find an object using its id.

The getDates function implements the calendar feature which you can easily build using HTML and CSS. Each date is shown as a button which on clicking calls the checkbooked function which checks the slots that are already booked. This depends on the booking functionality so I'll describe it later.

Sending booking request to owner

At the bottom of the venue details page there is a form which any customer can fill to send a booking request.

<form id="venueBookForm">
    <h4 style="color: #00a090;">Book this venue</h4>
    <div class="row mb-3">
        <div class="col-md-6">
            <label for="custName" class="form-label">Full name</label>
            <input type="text" id="custName" name="customerName" class="form-control">
        </div>
        <div class="col-md-6">
            <label for="email" class="form-label">Email Id</label>
            <input type="email" id="email" name="EmailId" class="form-control">
        </div>
    </div>

    ...
    
    //Insert label and input fields to collect other details

    ...

    <div class="my-2 text-danger" id="bookingError"></div>
    <button type="button" onclick="bookVenue()" class="btn text-light mb-2" id="bookVenueBtn">Book slot</button>
    <div class="my-2 text-success" id="bookingSuccess"></div>
</form>
function bookVenue() {
    document.getElementById("bookingError").innerHTML = "";
    const name = document.getElementById("custName").value;
    const email = document.getElementById("email").value;
    const date = document.getElementById("date").value;
    const timeStart = document.getElementById("starttime").value
    const timeEnd = document.getElementById("endtime").value;
    const details = document.getElementById("purpose").value;

    if (!name || !email || !date || !timeStart || !timeEnd || !details) {
        document.getElementById("bookingError").innerHTML = "Please fill all the fields.";
    }
    else {
        const user = Parse.User.current();

        const Venues = Parse.Object.extend("Venues");
        const q = new Parse.Query(Venues);
        q.get(venueId).then(function success(object) {
            var ownerOfVen = object.get("owner");

            const Booking = Parse.Object.extend("Booking");
            const booking = new Booking();

            var acl = new Parse.ACL();
            acl.setReadAccess(user, true);
            acl.setReadAccess(ownerOfVen, true);
            acl.setWriteAccess(ownerOfVen, true);

            booking.set("ACL", acl);
            booking.set("fullName", name);
            booking.set("email", email);
            booking.set("date", date);
            booking.set("timeSlot", timeStart + " - " + timeEnd);
            booking.set("details", details);
            booking.set("venue", object);
            booking.set("owner", ownerOfVen);
            booking.set("bookedBy", user);
            booking.set("approvedStatus", false);

            booking.save().then(function success(booking) {
                document.getElementById("venueBookForm").reset();
                document.getElementById("bookingSuccess").innerHTML = "Booking done successfully!";
                console.log("Booking done!");
            }, function error(err) {
                console.log("Error: ", err);
            });
        }, function error(err) {
            console.log(err);
        });
    }
}

The Booking class will be used to store details about booking requests. The approvedStatus field is a boolean value which if set to true, implies that the booking has been approved. As one of the field here is the email address of the customer who is booking the venue, we need to make sure that this data is private and can be read only by them and the owner. Also, the write access should be given only to the owner, as only they should be able to update the approvedStatus field.

But we'll have to show the timeSlots that have already been booked right? Yes, and to do so I have created another class named ApprovedBookings which contains only the venueId, timeslot, date, and a pointer to the Booking object and this class is publicly readable.

Approve requests

In the owner's dashboard under the booking requests tab all the requests made to any of their venues would be displayed.
Getting these booking details is very similar to how we fetched all the venues so I won't be going over it. Today's events tab is again the same thing but with a condition to find out only rows where date==today's date.

In the following image, the red box shows a request that hasn't been approved yet.
sked_newBookingReq

On clicking the approve button the approvedStatus is set to true and a new row is added to the ApprovedBookings class.

function approveReq(el, id) {

    if (el.innerHTML == "Approved") {
        return;
    }

    const Booking = Parse.Object.extend("Booking");
    const q = new Parse.Query(Booking);
    q.get(id).then((object) => {
        object.set("approvedStatus", true);
        object.save().then((booking) => {

            //create a row in ApprovedBookings class which has public read access
            const ApprovedBookings = Parse.Object.extend("ApprovedBookings");
            const approved = new ApprovedBookings();

            const acl = new Parse.ACL();
            acl.setPublicReadAccess(true);
            acl.setWriteAccess(Parse.User.current(), true);

            approved.set("date", booking.get("date"));
            approved.set("timeSlot", booking.get("timeSlot"));
            approved.set("venueID", booking.get("venue").id);
            approved.set("parent", object);
            approved.setACL(acl);
            approved.save().then(function () {
                console.log("approved and saved!");
            }, function error(err) {
                console.log(err);
            });

            el.innerHTML = "Approved";
            el.classList.remove("cardpink-btn");
            el.classList.add("cardpurple-btn");
            const card = document.getElementById(id);
            card.classList.remove("cardpink-bg");
            card.classList.add("cardpurple-bg");
        }, function error(err) {
            console.log(err);
        });
    });
}

After approval:
sked_approvedBooking

The time slots of approved bookings can be read by any user. Here's an example of how the slots already booked will appear upon clicking any date in the calendar.
Already booked slots

The main part of the checkbooked function which is called on clicking any date in the calendar is as follows:

const apprBooking = Parse.Object.extend("ApprovedBookings");
const query = new Parse.Query(apprBooking);
query.equalTo("venueID", venueId);
query.equalTo("date", datecheck);
query.find().then(successCallback,errorCallBack);

veneuId is a global variable containing the id of venue whose value was set in the insertDetails function.date refers to the date in the calendar which was clicked.

Deleting past bookings

Once an event has completed, maybe the owner doesn't need the booking information anymore so we have to provide a delete option for expired bookings. We will have to destroy it from both the Booking class and ApprovedBookings class. As ApprovedBookings has a pointer to its parent in Bookings, we'll start by deleting it first.
But it is possible that a booking was never approved. Then it would be present only in the Bookings class.

function deleteBooking(bookingid) {
    const Booking = Parse.Object.extend("Booking");
    const query = new Parse.Query(Booking);

    query.get(bookingid).then((bking) => {

        const status = bking.get("approvedStatus");

        //If approved,first remove record from ApprovedBookings class.
        if (status) {
            const apprBookings = Parse.Object.extend("ApprovedBookings");
            const q = new Parse.Query(apprBookings);
            q.equalTo("parent", bking);
            q.find().then((result) => {
                result[0].destroy().then(() => {
                    console.log("Deleted booking from ApprovedBookings");

                    //Next remove from Booking class
                    bking.destroy().then(() => {
                        const bookingcard = document.getElementById(bookingid);
                        bookingcard.parentElement.removeChild(bookingcard);
                        console.log("Deleted from Booking");
                    });
                });
            }, (err) => {
                console.log(err);
            });
        }
        else { //just remove the non approved booking from Booking class
            bking.destroy().then(() => {
                const bookingcard = document.getElementById(bookingid);
                bookingcard.parentElement.removeChild(bookingcard);
                console.log("Deleted from Booking");
            }, (err) => {
                console.log(err);
            });
        }
    });
}

Conclusion

I hope you've gotten some idea about how SashiDo can be used to perform various tasks in an easy way. This was a simple fun project so I haven't implemented any cloud code validation functions. Nevertheless, it was a great learning experience. There are a couple of more features that could have been added like allowing owners to edit venue details through the webpage itself and displaying time in the AM-PM format. I think it would be cool to have a feature that can show room usage statistics for each of the venues. Let me know in comments if you try any of those or add some of your own ideas!

Happy Coding!

Useful links

How to Build a Venue Booking App with SashiDo - Part 1
Github repo
Application Demo Video
Parse Javascript SDK documentation
SashiDo's Getting Started Guide
Arrow functions
Template literals

Nishka Kotian

College student exploring computer science

Find answers to all your questions

Our Frequently Asked Questions section is here to help.