Custom App

With App Creator you can enhance the UX (User-Experience) that suits your business flow. Lets now create an application that simplifies the steps Agent takes to get Movie Show Booked.

Mockup Plan

Niche UI can improve productivity to good extent. Planning & Design is the first step.

Create App

Navigate Main Menu > Platform > App Creator

Click Add App

Fill the form and Save

Click movie_shows app to edit.

views/index.html

Copy-and-paste the HTML code below which uses Bootstrap, jQuery, Select2 plugins with VCAP and index.js runtime.

<!DOCTYPE html>
<html>
    <head>
        <title>Movie Shows</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css">
		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css">
    	<link rel="stylesheet" href="resources/index.css">
    </head>
    <body>
        <div class="container">
			<div class="row">
            	<form id="movieshowform" class="col">
					<legend>Track Movies Booking</legend>
					<fieldset>
					<p>
						<select id="formovie" class="form-control" data-placeholder="Choose Movie" required></select>
					</p>
					<p>
						<select id="forcontact" class="form-control" data-placeholder="Choose Contact" required></select>
					</p>
					<p>
						<input id="showdate" type="date" required>
					</p>
					<button type="submit">OK</button>
					</fieldset>
				</form>
				<div class="col" id="movieinfo">
					<h4>About Movie</h4>
					<div class="overview"></div>
					<div><img class="poster" height="480px" width="480px"></div>
				</div>
			</div>
        </div>

        <script src="resources/jquery.min.js"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js"></script>
	    <script src="resources/vcap.js"></script>
        <script src="resources/index.js"></script>
    </body>
</html>

Click Crtl+S to save IDE changes.

resources/index.js

Copy-and-paste index.js handler code which initializes the form inputs and dynamic behaviour for the app.

window.addEventListener('load', () => {
	
	/**
	 * Config and State management for Searching.
	 */
	var searchState = {
		Contacts: {
			fields: ["firstname", "lastname"],
			filterid: 7,  /* Contacts: All Contacts filter */
			term: null,
			progress: false,
			result: null
		},
		vtcmmovies: {
			fields: ["fld_vtcmmoviesname"],
			filterid: 144, /* Movies: All Movies filter */
			term: null,
			progress: false,
			result: null
		}
	}
	
	/* helper function to build (q) clause with or-term startwith search */
	function searchWithOrClause(fields, term) {
		var q_and = [];
		var q_or = [];
		for (var i in fields) {
			q_or.push([fields[i], "startwith", term]);
		}
		return [ q_and, q_or ];
	}
	
	/* build custom transport for select2 specific to module and fields */
	function searchParamsInModule (module, fields, filterid) {
		/* transport function that gets callback from select2 */
		return function (params, success, failure) {
			var term = params.data.term;

			/* avoid search if last-one has not completed to avoid server overload */
			if (searchState[module].progress) {
				return;
			}

			/* if new-term is has not completely changed consider earlier result */
			if (searchState[module].term && term.indexOf(searchState[module].term) === 0) {
				return success(searchState[module].result);
			}
			
			/* prepare for async search */
			var response = new Promise((resolve, reject) => {
				/* mark the state */
				searchState[module].progress = true;
				searchState[module].term = term;

				/* do the search */
				VCAP.userapi.records.get({
					module: module, filterid: filterid,
					q: JSON.stringify( searchWithOrClause(fields, term) )
				}, (e, records) => {
					/* update the state */
					searchState[module].progress = false;
					searchState[module].result = records;

					/* return result */
					resolve(records);
				});
			});

			/* bind to success callback */
			response.then(success);

			return response;
		}
	}
	
	/* custom transformer to convert record to show in select2 dropdown */
	function transformRecordsToSelect2Options() {
		/* callback function after search */
		return function(records) {
			/* add text field */
			for (var i in records) records[i].text = records[i].label;
			return {results: records }
		}
	}
	
	/* select2 plugin activation for Movies */
	jQuery("#formovie").select2({
		minimumInputLength: 3,
		allowClear: true,
		ajax: {
			transport: searchParamsInModule("vtcmmovies", searchState["vtcmmovies"].fields, searchState["vtcmmovies"].filterid),
			processResults: transformRecordsToSelect2Options()
		}
	}).on("change", function(ev) {
		/* If movie selection changes */
		var choice = jQuery(this).select2('data');
		
		/* hide the last movie info */
		jQuery("#movieinfo").hide();
		
		/* movie was unselected or cleared? */
		if (!choice.length) return;
		
		/* 
		 * There was movie selection change
		 * use customapi created earlier for movie search 
		 * show the overview and poster image. 
		 */ 
		var movie = choice[0];
		VCAP.customapi.get("SearchTMDB", {
			moviename: movie["text"]
		}, (e, movieres) => {
			if (e) return alert(e.message);
			var movieinfo = JSON.parse(movieres.content).results[0];
			jQuery("#movieinfo .overview").text(movieinfo.overview);
			jQuery("#movieinfo .poster").attr(
				"src", "https://image.tmdb.org/t/p/w500" + movieinfo.poster_path);
			jQuery("#movieinfo").show();
		});
	});
	
	/* select2 plugin activation for Contact lookup */
	jQuery("#forcontact").select2({
		minimumInputLength: 3,
		allowClear: true,
		ajax: {
			transport: searchParamsInModule("Contacts", searchState["Contacts"].fields, searchState["Contacts"].filterid),
			processResults: transformRecordsToSelect2Options()
		}
	});

	/* form submit handler */
	jQuery("#movieshowform").submit(function(ev) {
		/* take custom control */
		ev.preventDefault();
		
		/* to disable when processing */
		var fieldset = jQuery("fieldset", this);
		
		/* field inputs */
		var contactid = jQuery("#forcontact").val();
		var movieid = jQuery("#formovie").val();
		var showdate = jQuery("#showdate").val();
		
		/* invalid state - return */
		if (!contactid || !movieid) return;
		
		/* about to process - disable changes */
		fieldset.attr("disabled", true);
		
		/* create event */
		VCAP.userapi.records.post({
			module      : "Events",
			subject     : "Movie Show For " + jQuery("#forcontact").select2("data")[0].text,
			eventstatus : "Booked",
			activitytype: "Movie Show",
			date_start  : showdate,
			time_start  : "09:00", /* todo: accept input */
			due_date    : showdate,
			time_end    : "10:30", /* todo: determine based on movie length */
			cf_calendar_movie: movieid,
			contact_id: contactid
		}, (e, event) => {

			/* renable form */
			fieldset.attr("disabled", null);
			
			/* error - show message */
			if (e) return alert(e.message);

			/* success - show feedback */
			fieldset.html(
				"<a target='_blank' href='/view/detail?module=Events&id=" + event.id  +"'>" + event.subject +" - Booked</a><br>" +
				"<button onclick='location.reload()'>New</button>"
			);
		});
	});
});

Click Crtl+S to save IDE changes.

Publish & Launch App

After saving file changes you need to Publish app for reflecting them others.

You can Launch app (or reload app if launched previously)

Awesome! we have now achieved enhancing user-experience with this niche custom app.

FAQs

Why am I not seeing published changes?

During development Browser cache can trouble new changes.

  • Hard refresh the app after launch.
  • You can disable cache that should help.

VCAP - Custom App Runtime.

VCAP JS

How to find the fieldname?

How to find the modulename and filterid?