Skip to content

4. How Reserved Seating Works

Armando Fox edited this page Oct 4, 2021 · 5 revisions

A Voucher (really an Item) has a seat column which is blank for general-admission shows. For reserved seating performances, this field contains a seat number. General admission (GA) vs. reserved seating (RS) can be specified at the per-performance level, so some performances of a production can be GA while others are RS.

Differences in reserved seating workflow

There are four workflows in which the user may have to choose seat(s) for an RS show:

  1. Subscriber using a subscriber voucher to make a reservation ("My Tickets" screen)
  2. Box office adding comps for a patron ("Add comps" screen)
  3. Box office doing a walkup sale during front-of-house
  4. Box office checking in a subscriber using subscriber vouchers during front-of-house

(The seatmap is also displayed during the "Add/Edit Seatmaps" dialog but we won't cover that here.)

In cases 1 and 2, selecting a show date from a dropdown menu causes a seatmap to appear if that showdate is RS, or disappear if it is GA. (The file components/_seatmap.html.haml is an invisible div that is part of every page where this could occur and is made visible/invisible as needed.) The JavaScript files whose name includes seatmap contain the code for interacting with the seatmap, though most of the work is done by the external library jquery.seat-charts.js. (BUG: the various jquery.* files should be vendored.)

As the user interacts with the seatmap, a designated "Seats selected" field on the page is filled in. The JS code ensures that the correct number of seats is selected. Upon form submission (confirm reservation, finalize sale, or whatever), the contents of that field are submitted as part of the form and saved for each associated voucher. Model validations check that for GA shows, no seats are specified (field is blank), and for RS shows, the number of seats chosen matches the number of tickets in the order, the seat numbers are all valid for the show's seatmap, and the seats are not occupied. (The JavaScript code won't allow choosing an occupied seat, but race conditions can happen.)

Code flow for using reserved seating

Any flow that might use reserved seating needs to do the following:

  1. Render the partial components/seatmap. It will be hidden by default, but it must be included in any view that might need to show the seatmap.
  2. Attach particular CSS selectors to various elements on the page, as described below.
  3. Provide a setup function that sets up various slots in A1.seatmap as described below, and causes the seatmap to be rendered for interaction with the user.
  4. Bind that setup function to the appropriate change event, such as the user selecting a date from a dropdown menu. The function is invoked on every change, because if it detects that (e.g.) the user selects a GA show, it might be necessary (e.g.) to re-hide the seatmap.
  5. (Optional) Provide a reset function that resets any state and visuals if the user clicks "Cancel" to exit the seat selection dialog without choosing seat(s).

Setup function part 1: set up reserved-seating info

The setup function should fill in the following slots of the A1.seatmap data structure. Non-required slots can be left empty. Each of the following slots should be set to a jQuery selector expression, e.g. $('.seat-display'). The expressions will be evaluated the first time the seatmap becomes visible on a given page.

  • max: the number of seats to be selected.

  • onSelect: a function called when a seat is either selected or un-selected. A common thing to do here is grab the value of A1.seatmap.selectedSeats (a JS array of the seat numbers selected so far) or A1.seatmap.selectedSeatsAsString (a comma-separated version of that array as a displayable string) to update the value of a "Seats selected so far" field.

  • allSeatsSelected: a function that will be called when the correct number of seats has been selected. Note that it is possible that seats may be 'un-selected' later. This function can enable a Confirm/Finalize button, or whatever. This is always called after onSelect, so when the final seat is selected, onSelect will be called first and then allSeatsSelected will be called.

  • resetAfterCancel: optional - a function that will be called if the user cancels seat selection. It can restore the page to look however it looked before the seatmap was triggered. The JS code takes care of hiding the seatmap, resetting the "selected seats" field (seatDisplayField jQuery selector) to blank, etc., so this function just has to do any additional cleanup, such as re-enabling/disabling other buttons and fields. For example, on the Subscriber Reservations flow, the dropdown menu of how many seats to reserve is temporarily frozen (disabled) while seat selection is in progress; cancelling (or completing) seat selection un-freezes (re-enables) this menu.

Setup function part 2: render correct seatmap

Once the setup function has set the above slots, it must get its hands on the JSON representation of an A1 seatmap, as returned by the Seatmap model. The easiest way to get this is to call the endpoint /ajax/seatmap/:id, which returns a JSON object representing the seatmap associated with showdate ID :id. In particular, this object has the following slots, which you don't need to interpret or care about if you use the AJAX endpoint to fetch the object:

  • map: the JSON representation of a seatmap used by the jQuery Seat Charts plugin
  • seats: the JSON representation of seat classes (categories, price points) used by the jQuery Seat Charts plugin
  • unavailable: an array of the seat numbers of unavailable seats
  • image_url: the URL of an image to use as the background to display behind the seating chart (for example, to show where the stage is located)

If you also pass an optional zone parameter to the AJAX call, the available seats will be restricted to the zone whose shortname matches the parameter value. (See below for more info about zones.)

(Note that some pages eagerly load this information for all showdates and keep it in a hidden form field, rather than doing an AJAX call. That's probably uglier than needed. Also, if you call the endpoint on a showdate ID that has no associated seatmap because it's reserved seating, you will get the object {"map": null}.)

Once the setup function has this JSON data (let's call it jsSeats), it should do the following:

A1.seatmap.configureFrom(jsSeats);
A1.seatmap.seats = $('#seatmap').seatCharts(A1.seatmap.settings);  // this should be factored out as common code
A1.seatmap.setupMap();                                             // this should be factored out as common code

And when it is time to actually display the seatmap itself

$('#seating-charts-wrapper').removeClass('d-none').slideDown();  // reveal the seatmap
A1.seatmap.max = 2;  // number of seats the user is supposed to choose
A1.seatmap.setupMap();    // display bgnd image and properly center seatmap on page

That's it. If the user selects the right number of seats, the JS code will enable the "submit" button (confirmSeatsButton), the form will be submitted, and the controller action can check the "seats selected" element (seatDisplayField) for the actual seat numbers chosen. If the user cancels without finishing selection, the seatmap will be hidden, selected-seat data will be cleared both internally and from the seatDisplayField, and the resetAfterCancel function will be called if one was provided.

Regarding Seating Zones

Every seat in a seat map is associated with exactly one Seating Zone. (A default seating zone of "Reserved" is created when the theater is provisioned. Theaters can add more zones if they wish.) A zone has a display name and a short name, both of which must be unique.

If a Vouchertype is associated with a Seating Zone, it can only be used to redeem seats in that zone. A vouchertype with no Seating Zone can be used to redeem any seat. Also, zone restrictions can be overridden by staff, as with all other constraints.

Because of limitations in the underlying seating chart library, this is how zones are handled:

  1. When a seat map is parsed, it looks for each spreadsheet cell to have the format "shortname:seatnumber", e.g. "Reserved:A101", with an optional trailing "+" to indicate Accessible seats.
  2. The json attribute of the seatmap is constructed to include the zone name, so that seats have labels such as (eg) "Reserved-A101". Note that because the library uses each seat's hover label as the HTML ID, the complete string of the hover label must also be a valid HTML ID, so the only zone display name must match /^[A-Za-z0-9_]+$/. This representation is only used to control what gets displayed when the patron hovers over a seat.
  3. As well, a zones hash is created whose keys are zone shortnames and whose values are an array of all the seat numbers in that zone. This representation is used to determine which seats should be made unavailable at seat selection time due to vouchertype zone restrictions.
  4. When a seat is actually clicked, the zone name is stripped away so only the seat number (eg A101) is incorporated into the reservation transaction and saved as part of the resulting voucher.
  5. During ticket purchase, only vouchers restricted to the same zone, or vouchers with no zone restrictions, can be combined in a single transaction. So to purchase 1 Premium and 1 Regular seat for the same performance requires two separate transactions. This is effected both by client-side code that dynamically disables menus as vouchers are chosen, and by server-side code that validates that all redemptions added to the order are for "zone-compatible" voucher types.

In other words: zones only control what seats the patron can buy/reserve with a voucher type, but once the patron has a seat with a seat assignment, it doesn't matter how it got that way (zones are irrelevant). A corollary is that if box office changes a patron's seat to be a zone for which the associated voucher is nominally invalid, it doesn't cause any integrity problems because the zone info is only used at purchase/reservation time.

Note that the concept of being an accessible seat is orthogonal to the zone concept. Accessible seats can be included in any zone and are subject to the usual zone restrictions. (To allow a patron to get around this, the theater could define an "Accessible seating" ticket type that is valid for any seat in the house, so that a patron needing an accessible seat can also book a companion non-accessible seat in the same transaction.)