diff --git a/Gemfile b/Gemfile
index dfc8d1bfb..7695fa176 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,6 +17,7 @@ group :development, :test do
gem 'rubocop', '~> 1.45', require: false
gem 'rubocop-performance', '~> 1.16', require: false
gem 'rubocop-rails', '~> 2.17', require: false
+ gem 'rubocop-rspec', '~> 1.42', require: false
end
group :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index 74a1d4551..a9808eaa4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -38,6 +38,7 @@ PATH
activerecord_json_validator (~> 2.1, >= 2.1.3)
aws-sdk-s3
byebug
+ counter_culture (~> 3.2)
dry-validation (~> 1.10)
elasticsearch (~> 8.5)
exception_notification
@@ -227,6 +228,9 @@ GEM
console (1.23.2)
fiber-annotation
fiber-local
+ counter_culture (3.5.2)
+ activerecord (>= 4.2)
+ activesupport (>= 4.2)
crack (0.4.5)
rexml
crass (1.0.6)
@@ -601,6 +605,8 @@ GEM
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
+ rubocop-rspec (1.42.0)
+ rubocop (>= 0.87.0)
ruby-progressbar (1.13.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
@@ -808,6 +814,7 @@ DEPENDENCIES
rubocop (~> 1.45)
rubocop-performance (~> 1.16)
rubocop-rails (~> 2.17)
+ rubocop-rspec (~> 1.42)
shoulda-matchers (~> 5.0)
spree_cm_commissioner!
spree_dev_tools!
diff --git a/app/assets/images/cm-driver-seat-icons.svg b/app/assets/images/cm-driver-seat-icons.svg
new file mode 100644
index 000000000..f64ae376b
--- /dev/null
+++ b/app/assets/images/cm-driver-seat-icons.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/images/cm-normal-seat-icons.svg b/app/assets/images/cm-normal-seat-icons.svg
new file mode 100755
index 000000000..052219489
--- /dev/null
+++ b/app/assets/images/cm-normal-seat-icons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/cm-vip-seat-icons.svg b/app/assets/images/cm-vip-seat-icons.svg
new file mode 100755
index 000000000..810c431de
--- /dev/null
+++ b/app/assets/images/cm-vip-seat-icons.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/spree_cm_commissioner/backend.js b/app/assets/javascripts/spree_cm_commissioner/backend.js
index aa9dea788..60adae16a 100644
--- a/app/assets/javascripts/spree_cm_commissioner/backend.js
+++ b/app/assets/javascripts/spree_cm_commissioner/backend.js
@@ -3,3 +3,7 @@
//= require spree_cm_commissioner/calendar
//= require spree_cm_commissioner/role_form
//= require spree_cm_commissioner/address_hide_form
+//= require spree_cm_commissioner/vehicle_seat
+//= require spree_cm_commissioner/selected_field_filter_origin_destination
+//= require spree_cm_commissioner/location_checkbox_manager
+//= require spree_cm_commissioner/trip_stop_selection
diff --git a/app/assets/javascripts/spree_cm_commissioner/location_checkbox_manager.js b/app/assets/javascripts/spree_cm_commissioner/location_checkbox_manager.js
new file mode 100644
index 000000000..3765feeb0
--- /dev/null
+++ b/app/assets/javascripts/spree_cm_commissioner/location_checkbox_manager.js
@@ -0,0 +1,45 @@
+const LocationCheckboxManager = {
+ initialize: function () {
+ const locationCheckboxes = document.querySelectorAll(".location");
+
+ locationCheckboxes.forEach(function (locationCheckbox) {
+ locationCheckbox.addEventListener("change", function () {
+ LocationCheckboxManager.handleCheckboxChange(this);
+ });
+
+ LocationCheckboxManager.handleCheckboxChange(locationCheckbox);
+ });
+ },
+
+ handleCheckboxChange: function (changedCheckbox) {
+ const pointCheckbox = document.querySelector(".stop");
+ const stopCheckbox = document.querySelector(".station");
+ const branchCheckbox = document.querySelector(".branch");
+
+ if (!stopCheckbox || !branchCheckbox || !pointCheckbox) {
+ alert("Please select at the last one.");
+ event.preventDefault();
+ }
+
+ const isLocationChecked = changedCheckbox.checked;
+
+ if (isLocationChecked) {
+ stopCheckbox.checked = false;
+ stopCheckbox.disabled = true;
+
+ branchCheckbox.checked = false;
+ branchCheckbox.disabled = true;
+
+ pointCheckbox.checked = false;
+ pointCheckbox.disabled = true;
+ } else {
+ stopCheckbox.disabled = false;
+ branchCheckbox.disabled = false;
+ pointCheckbox.disabled = false;
+ }
+ },
+};
+
+document.addEventListener("spree:load", function () {
+ LocationCheckboxManager.initialize();
+});
diff --git a/app/assets/javascripts/spree_cm_commissioner/selected_field_filter_origin_destination.js b/app/assets/javascripts/spree_cm_commissioner/selected_field_filter_origin_destination.js
new file mode 100644
index 000000000..4783d3359
--- /dev/null
+++ b/app/assets/javascripts/spree_cm_commissioner/selected_field_filter_origin_destination.js
@@ -0,0 +1,37 @@
+var SelectFieldFilterOriginDestination = {
+ initialize: function (originSelector, destinationSelector, selectedOriginId, selectedDestinationId) {
+ function filterOptions(originId, destinationSelector) {
+ $(destinationSelector + ' option').each(function () {
+ if ($(this).val() == originId) {
+ $(this).prop('disabled', true).hide();
+ }
+ else {
+ $(this).prop('disabled', false).show();
+ }
+ });
+ }
+
+ $(originSelector).val(selectedOriginId);
+ $(destinationSelector).val(selectedDestinationId);
+
+ filterOptions(selectedOriginId, destinationSelector);
+ filterOptions(selectedDestinationId, originSelector);
+
+
+ $(document).on('change', originSelector, function () {
+ var originId = $(this).val();
+ filterOptions(originId, destinationSelector);
+ });
+
+ $(document).on('change', destinationSelector, function () {
+ var destinationId = $(this).val();
+ filterOptions(destinationId, originSelector);
+ });
+ }
+}
+
+document.addEventListener("spree:load", function () {
+ var selectedOriginId = $('#product_origin_id').val();
+ var selectedDestinationId = $('#product_destination_id').val();
+ SelectFieldFilterOriginDestination.initialize('#product_origin_id', '#product_destination_id', selectedOriginId, selectedDestinationId);
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/spree_cm_commissioner/trip_stop_selection.js b/app/assets/javascripts/spree_cm_commissioner/trip_stop_selection.js
new file mode 100644
index 000000000..b9e9f206d
--- /dev/null
+++ b/app/assets/javascripts/spree_cm_commissioner/trip_stop_selection.js
@@ -0,0 +1,41 @@
+var boardingAndDropOffSelectionFilter = {
+ initialize: function (boardingSelector, dropOffSelector) {
+ var selectedOriginIds = $(boardingSelector).val();
+ var selectedDestinationIds = $(dropOffSelector).val();
+
+ function filterOptions(selectedIds, selector) {
+ $(selector + " option").each(function () {
+ var optionValue = $(this).val();
+ // Check if the optionValue exists in the selectedIds array
+ if (selectedIds.includes(optionValue)) {
+ $(this).prop("disabled", true).hide();
+ } else {
+ $(this).prop("disabled", false).show();
+ }
+ });
+ }
+
+ $(boardingSelector).val(selectedOriginIds);
+ $(dropOffSelector).val(selectedDestinationIds);
+
+ filterOptions(selectedOriginIds, dropOffSelector);
+ filterOptions(selectedDestinationIds, boardingSelector);
+
+ $(document).on("change", boardingSelector, function () {
+ var originIds = $(this).val();
+ filterOptions(originIds, dropOffSelector);
+ });
+
+ $(document).on("change", dropOffSelector, function () {
+ var destinationIds = $(this).val();
+ filterOptions(destinationIds, boardingSelector);
+ });
+ },
+};
+
+document.addEventListener("spree:load", function () {
+ boardingAndDropOffSelectionFilter.initialize(
+ "#boarding_stops",
+ "#drop-off_stops"
+ );
+});
diff --git a/app/assets/javascripts/spree_cm_commissioner/vehicle_seat.js b/app/assets/javascripts/spree_cm_commissioner/vehicle_seat.js
new file mode 100644
index 000000000..cf59de7b3
--- /dev/null
+++ b/app/assets/javascripts/spree_cm_commissioner/vehicle_seat.js
@@ -0,0 +1,291 @@
+const VehcileSeatLayoutHandler = {
+ Seat: class {
+ constructor(row, column, label) {
+ this.row = row;
+ this.column = column;
+ this.label = label;
+ this.layer = VehcileSeatLayoutHandler.layer.value;
+ this.seat_type = 0;
+ this.vehicle_type_id = VehcileSeatLayoutHandler.vehicleTypeId;
+ }
+ },
+
+ initialize: function () {
+ this.keyInit();
+ this.listenToRow();
+ this.listenToColumn();
+ this.listenToSuffix();
+ this.listenToLayer();
+ this.listenToLabel();
+ this.listenToSubmitButton();
+ this.typeSelect();
+ },
+
+ keyInit: function () {
+ this.row = document.getElementById("row");
+ this.column = document.getElementById("column");
+ this.seatsContainer = document.querySelector(".seatsContainer");
+ this.label = document.getElementById("label");
+ this.layer = document.getElementById("layer");
+ this.submitButton = document.querySelector(".saveButton");
+ this.segments = window.location.pathname.split("/");
+ this.vehicleTypeId = this.segments[this.segments.length - 2];
+ this.suffix = document.getElementById("suffix");
+ this.selectedRow = null;
+ this.selectedColumn = null;
+ this.seats = [];
+ },
+
+ listenToSuffix: function () {
+ this.suffix.addEventListener("input", () => {
+ let newLabel = this.labelGenerator(this.suffix.checked);
+ if (
+ this.row.value > 0 &&
+ this.column.value > 0 &&
+ this.seats &&
+ this.label.value
+ ) {
+ for (let i = 0; i < this.row.value; i++) {
+ for (let j = 0; j < this.column.value; j++) {
+ if (
+ this.seats[i][j].seat_type != 1 &&
+ this.seats[i][j].seat_type != 3
+ ) {
+ let tmpLabel = this.seats[i][j].label.replace(label.value, "");
+ this.seats[i][j].label = newLabel(tmpLabel, label.value);
+ }
+ }
+ }
+ this.getSeats();
+ }
+ });
+ },
+
+ listenToLayer: function () {
+ this.layer.addEventListener("input", () => {
+ if (this.row.value > 0 && this.column.value > 0 && this.seats) {
+ this.addLayer();
+ console.log("updated layer");
+ console.log(this.seats);
+ }
+ });
+ },
+
+ listenToRow: function () {
+ this.row.addEventListener("input", () => {
+ if (this.row.value > 0 && this.column.value > 0) {
+ this.constructSeats();
+ this.getSeats();
+ }
+ });
+ },
+
+ listenToColumn: function () {
+ this.column.addEventListener("input", () => {
+ if (this.row.value > 0 && this.column.value > 0) {
+ this.constructSeats();
+ this.getSeats();
+ }
+ });
+ },
+
+ listenToLabel: function () {
+ this.label.addEventListener("input", () => {
+ let newLabel = this.labelGenerator(this.suffix.checked);
+ if (this.row.value > 0 && this.column.value > 0 && this.seats) {
+ let tmpLabel = 0;
+ this.seats.forEach((seatRow) => {
+ seatRow.forEach((seat) => {
+ if (seat.seat_type != 1 && seat.seat_type != 3) {
+ seat.label = newLabel(++tmpLabel, label.value);
+ } else {
+ seat.label = "NA";
+ }
+ });
+ });
+ this.getSeats();
+ }
+ });
+ },
+
+ listenToSubmitButton: function () {
+ this.submitButton.addEventListener("click", () => {
+ if (seats.length > 0) {
+ this.submit();
+ }
+ });
+ },
+
+ labelGenerator: function (isSuffix) {
+ let newLabel;
+ isSuffix
+ ? (newLabel = (tmpLabel, labelValue) => `${tmpLabel}${labelValue}`)
+ : (newLabel = (tmpLabel, labelValue) => `${labelValue}${tmpLabel}`);
+ return newLabel;
+ },
+
+ constructSeats: function () {
+ seats = [];
+ let tmpLabel = 0;
+ let newLabel = this.labelGenerator(suffix.checked);
+ for (let i = 1; i <= this.row.value; i++) {
+ let columns = [];
+ for (let j = 1; j <= this.column.value; j++) {
+ columns.push(new this.Seat(i, j, newLabel(++tmpLabel, label.value)));
+ }
+ seats.push(columns);
+ }
+ this.seats = seats;
+ },
+
+ getSeats: function () {
+ $.ajax({
+ url: "/transit/vehicle_types/vehicle_seats/load_seat",
+ type: "POST",
+ data: {
+ row: this.row.value,
+ column: this.column.value,
+ label: this.label.value,
+ seats: JSON.stringify(this.seats),
+ },
+ success: (response) => {
+ this.seatsContainer.innerHTML = response;
+ this.addSeatClickListener();
+ this.editLabel();
+ },
+ error: function (xhr, status, error) {
+ show_flash("error", error);
+ },
+ });
+ },
+
+ submit: function () {
+ $.ajax({
+ url: `/transit/vehicle_types/${this.vehicleTypeId}/vehicle_seats`,
+ type: "POST",
+ data: {
+ seats: JSON.stringify(this.seats),
+ },
+ success: (response) => {
+ location.reload();
+ },
+ error: (xhr, status, error) => {
+ console.error("Error updating seat layer:", error);
+ },
+ });
+ },
+
+ addLayer: function () {
+ this.seats.forEach((seatRow) => {
+ seatRow.forEach((seat) => {
+ seat.layer = layer.value;
+ });
+ });
+ },
+
+ addSeatClickListener: function () {
+ let self = this;
+ let selectedSeat = null;
+ document.querySelectorAll(".seat-container").forEach((seat) => {
+ let seatIcon = (seat) => {
+ return seat.querySelector(".seat-icon");
+ };
+ let seatLabel = (seat) => {
+ return seat.querySelector(".seat-label");
+ };
+
+ seat.addEventListener("click", function () {
+ if (selectedSeat && selectedSeat != this) {
+ seatIcon(selectedSeat).classList.remove("selected-seat");
+ seatLabel(selectedSeat).disabled = true;
+ }
+
+ seatIcon(this).classList.add("selected-seat");
+ seatLabel(this).disabled = false;
+ selectedSeat = this;
+
+ self.selectedRow =
+ parseInt(this.querySelector(".seat-icon").dataset.seatRow) - 1;
+ self.selectedColumn =
+ parseInt(this.querySelector(".seat-icon").dataset.seatColumn) - 1;
+ let seat_type =
+ self.seats[self.selectedRow][self.selectedColumn].seat_type;
+ document.getElementById("type").value = seat_type;
+ });
+ });
+ },
+
+ editLabel: function () {
+ let self = this;
+ document.querySelectorAll(".seat-label").forEach((label) => {
+ label.addEventListener("change", () => {
+ self.seats[self.selectedRow][self.selectedColumn].label =
+ document.getElementById(
+ `${self.selectedRow + 1}${self.selectedColumn + 1}`
+ ).value;
+ self.getSeats();
+ });
+ });
+ },
+
+ typeSelect: function () {
+ document.getElementById("type").addEventListener("input", () => {
+ this.seats[this.selectedRow][this.selectedColumn].seat_type = parseInt(
+ document.getElementById("type").value
+ );
+ let newLabel = this.labelGenerator(this.suffix.checked);
+ let tmpLabel = 0;
+
+ for (let i = 0; i < this.row.value; i++) {
+ for (let j = 0; j < this.column.value; j++) {
+ if (
+ this.seats[i][j].seat_type != 1 &&
+ this.seats[i][j].seat_type != 3
+ ) {
+ this.seats[i][j].label = newLabel(++tmpLabel, label.value);
+ } else {
+ this.seats[i][j].label = "NA";
+ }
+ }
+ }
+ this.getSeats();
+ });
+ },
+};
+
+const VehicleSeatViewHandler = {
+ initialize: function () {
+ $(document).ready(function () {
+ if ($(".view-layer-btn").length > 0) {
+ $(".view-layer-btn").click(function () {
+ let layer = $(this).data("layer");
+ let layer_name = $(this).data("layer-name");
+ let column = $(this).data("layer-column");
+ let row = $(this).data("layer-row");
+ let seatsViewContainer = document.querySelector(".modal-body");
+ $.ajax({
+ url: "/transit/vehicle_types/layer",
+ type: "POST",
+ data: {
+ row: row,
+ layer_name: layer_name,
+ column: column,
+ seats: JSON.stringify(layer),
+ },
+ success: function (response) {
+ seatsViewContainer.innerHTML = response;
+ },
+ error: function (xhr, status, error) {
+ show_flash("error", error);
+ },
+ });
+ });
+ }
+ });
+ },
+};
+
+document.addEventListener("spree:load", function () {
+ VehcileSeatLayoutHandler.initialize();
+ VehicleSeatViewHandler.initialize();
+});
diff --git a/app/assets/stylesheets/spree_cm_commissioner/backend/commissioner_admin.css.scss b/app/assets/stylesheets/spree_cm_commissioner/backend/commissioner_admin.css.scss
index 9cd1b4c51..c2ff9d5de 100644
--- a/app/assets/stylesheets/spree_cm_commissioner/backend/commissioner_admin.css.scss
+++ b/app/assets/stylesheets/spree_cm_commissioner/backend/commissioner_admin.css.scss
@@ -1,3 +1,4 @@
@import "spree/backend/spree_admin";
@import "spree_cm_commissioner/backend/calendar";
@import "spree_cm_commissioner/backend/service_calendar";
+@import "spree_cm_commissioner/backend/vehicle_seat"
diff --git a/app/assets/stylesheets/spree_cm_commissioner/backend/vehicle_seat.scss b/app/assets/stylesheets/spree_cm_commissioner/backend/vehicle_seat.scss
new file mode 100644
index 000000000..74f5df9de
--- /dev/null
+++ b/app/assets/stylesheets/spree_cm_commissioner/backend/vehicle_seat.scss
@@ -0,0 +1,28 @@
+/* seats.css */
+
+.seat {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ background-color: #ccc;
+ margin: 2px;
+ cursor: pointer;
+}
+
+.selected-seat {
+ // background-color: #f00;
+ border: 1px dotted grey;
+}
+
+.hidden-seat{
+ width: 35px;
+ display: inline-block;
+}
+
+.empty-seat{
+ border: 1px solid black;
+}
+
+.hidden-input{
+ visibility: hidden;
+}
diff --git a/app/assets/stylesheets/spree_cm_commissioner/vehicle_seat.css b/app/assets/stylesheets/spree_cm_commissioner/vehicle_seat.css
new file mode 100644
index 000000000..644549c44
--- /dev/null
+++ b/app/assets/stylesheets/spree_cm_commissioner/vehicle_seat.css
@@ -0,0 +1,7 @@
+/*
+ * This is a manifest file that'll automatically include all the stylesheets available in this directory
+ * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
+ * the top of the compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require spree_cm_commissioner/vehicle/vehicle_seat
+*/
diff --git a/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb b/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb
new file mode 100644
index 000000000..ad8a5afbe
--- /dev/null
+++ b/app/controllers/concerns/spree_cm_commissioner/transit/taxon_bitwise.rb
@@ -0,0 +1,44 @@
+module SpreeCmCommissioner
+ module Transit
+ module TaxonBitwise
+ extend ActiveSupport::Concern
+
+ BIT_STOP = 0b001
+ BIT_STATION = 0b010
+ BIT_BRANCH = 0b100
+ BIT_LOCATION = 0b1000
+
+ included do
+ attr_accessor :stop, :station, :branch, :location
+
+ before_validation :at_least_one_checkbox_selected
+ end
+
+ def stop?
+ data_type & BIT_STOP != 0
+ end
+
+ def station?
+ data_type & BIT_STATION != 0
+ end
+
+ def branch?
+ data_type & BIT_BRANCH != 0
+ end
+
+ def location?
+ data_type & BIT_LOCATION != 0
+ end
+
+ private
+
+ def at_least_one_checkbox_selected
+ if stop.nil? && station.nil? && branch.nil? && location.nil?
+ nil
+ elsif stop.to_i.zero? && station.to_i.zero? && branch.to_i.zero? && location.to_i.zero?
+ errors.add(:base, 'At least one checkbox (stop, branch, or location) must be selected')
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/amenities_controller.rb b/app/controllers/spree/transit/amenities_controller.rb
new file mode 100644
index 000000000..3cc76c68d
--- /dev/null
+++ b/app/controllers/spree/transit/amenities_controller.rb
@@ -0,0 +1,79 @@
+module Spree
+ module Transit
+ class AmenitiesController < Spree::Transit::BaseController
+ before_action :load_data
+ before_action :load_vendor
+ before_action :load_amenity
+ before_action :setup_new_option_value, only: :edit
+
+ def load_data
+ @amenities = Spree::OptionType.all
+ end
+
+ def update_values_positions
+ ApplicationRecord.transaction do
+ params[:positions].each do |id, index|
+ Spree::OptionValue.where(id: id).update_all(position: index)
+ end
+ end
+
+ respond_to do |format|
+ format.html { redirect_to spree.transit_routes_url }
+ format.js { render plain: 'Ok' }
+ end
+ end
+
+ def update
+ load_amenity
+ if @amenity.update(amenity_params)
+ flash[:success] = 'Option Type updated successfully.'
+ else
+ flash[:error] = "Unable to update Option Type. Errors: #{@amenity.errors.full_messages.join(', ')}"
+ end
+ redirect_back(fallback_location: edit_transit_amenity_path(@amenity))
+ end
+
+ def location_after_save
+ edit_transit_amenity_path(@amenity)
+ end
+
+ def collection
+ load_data
+
+ @objects = model_class.where(kind: :vehicle_type)
+ @search = @objects.ransack(params[:q])
+ @collection = @search.result
+
+ @collection
+ end
+
+ def load_vendor
+ @vendor ||= vendors.find { |v| v[:slug] == session[:transit_current_vendor_slug] } || vendors.first
+ end
+
+ def model_class
+ Spree::OptionType
+ end
+
+ def object_name
+ 'option_type'
+ end
+
+ def setup_new_option_value
+ @amenity.option_values.build if @amenity.option_values.empty?
+ end
+
+ private
+
+ def load_amenity
+ ActiveRecord::Base.connected_to(role: :writing) do
+ @amenity = Spree::OptionType.amenities
+ end
+ end
+
+ def amenity_params
+ params.require(:option_type).permit(:name, :presentation, :kind, :attr_type, option_values_attributes: {})
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/amenity_values_controller.rb b/app/controllers/spree/transit/amenity_values_controller.rb
new file mode 100644
index 000000000..2a10738a4
--- /dev/null
+++ b/app/controllers/spree/transit/amenity_values_controller.rb
@@ -0,0 +1,16 @@
+module Spree
+ module Transit
+ class AmenityValuesController < Spree::Transit::BaseController
+ # This method is added here to allow `edit_polymorphic_path` to work
+ def edit_admin_option_value_path(option_value)
+ spree.edit_admin_option_type_url(option_value.option_type)
+ end
+
+ def destroy
+ option_value = Spree::OptionValue.find(params[:id])
+ option_value.destroy
+ render plain: nil
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/base_controller.rb b/app/controllers/spree/transit/base_controller.rb
new file mode 100644
index 000000000..3f270398c
--- /dev/null
+++ b/app/controllers/spree/transit/base_controller.rb
@@ -0,0 +1,53 @@
+module Spree
+ module Transit
+ class BaseController < Spree::Admin::ResourceController
+ helper_method :current_vendor, :vendors
+ before_action :required_vendor_user!
+ before_action :current_vendor
+ layout 'spree/layouts/transit'
+
+ def vendors
+ @vendors ||= spree_current_user.vendors.to_a
+ end
+
+ def page
+ params[:page] || 1
+ end
+
+ def per_page
+ params[:per_page] || 12
+ end
+
+ # @overrided
+ def required_vendor_user!
+ return unless vendors.empty?
+
+ raise SpreeCmCommissioner::UnauthorizedVendorError
+ end
+
+ def current_vendor
+ @current_vendor ||= vendors.find { |v| v[:slug] == session[:transit_current_vendor_slug] } || vendors.first
+ session[:transit_current_vendor_slug] ||= @current_vendor&.slug
+
+ @current_vendor
+ end
+
+ def collection_url(options = {})
+ if parent_data.present?
+ spree.polymorphic_url([:transit, parent, model_class], options)
+ else
+ spree.polymorphic_url([:transit, model_class], options)
+ end
+ end
+
+ def edit_object_url(object, options = {})
+ if parent_data.present?
+ spree.send "edit_transit_#{resource.model_name}_#{resource.object_name}_url",
+ parent, object, options
+ else
+ spree.send "edit_transit_#{resource.object_name}_url", object, options
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/branches_controller.rb b/app/controllers/spree/transit/branches_controller.rb
new file mode 100644
index 000000000..aeeaa50d5
--- /dev/null
+++ b/app/controllers/spree/transit/branches_controller.rb
@@ -0,0 +1,35 @@
+module Spree
+ module Transit
+ class BranchesController < Spree::Transit::BaseController
+ def new
+ @branch = SpreeCmCommissioner::Branch.new
+ super
+ end
+
+ def collection
+ return @collection if defined?(@collection)
+
+ current_vendor.branches
+
+ @search = current_vendor.branches.includes(:state).ransack(params[:q])
+ @collection = @search.result
+ end
+
+ def location_after_save
+ transit_branches_url
+ end
+
+ def model_class
+ SpreeCmCommissioner::Branch
+ end
+
+ def object_name
+ 'spree_cm_commissioner_branch'
+ end
+
+ def branch_params
+ params.require(:branch).permit(:name, :lat, :lon)
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/locations_controller.rb b/app/controllers/spree/transit/locations_controller.rb
new file mode 100644
index 000000000..07badbed6
--- /dev/null
+++ b/app/controllers/spree/transit/locations_controller.rb
@@ -0,0 +1,38 @@
+module Spree
+ module Transit
+ class LocationsController < Spree::Transit::BaseController
+ def load_location
+ @locations = Spree::State.all
+ end
+
+ def load_country
+ @countries = Spree::Country.where(id: current_vendor.default_country)
+ end
+
+ def new
+ @object.country_id = Spree::Country.find_by(id: current_vendor.default_country)&.id
+ end
+
+ def collection
+ return @collection if defined?(@collection)
+
+ load_location
+
+ @search = @locations.ransack(params[:q])
+ @collection = @search.result
+ end
+
+ def location_after_save
+ transit_locations_url
+ end
+
+ def model_class
+ Spree::State
+ end
+
+ def object_name
+ 'state'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/places_controller.rb b/app/controllers/spree/transit/places_controller.rb
new file mode 100644
index 000000000..3d4ef8591
--- /dev/null
+++ b/app/controllers/spree/transit/places_controller.rb
@@ -0,0 +1,151 @@
+module Spree
+ module Transit
+ class PlacesController < Spree::Transit::BaseController
+ before_action :load_place_taxonomy
+ helper 'spree/transit/sortable_tree'
+
+ def load_place_taxonomy
+ @place_taxonomy = Spree::Taxonomy.place
+ end
+
+ before_action :set_permalink_part, only: [:edit]
+ respond_to :html, :js
+ helper 'spree/transit/sortable_tree'
+ before_action :set_data_type, only: %i[update create]
+
+ def index; end
+
+ def new
+ @taxon = Spree::Taxon.new
+ @taxon.taxonomy = @place_taxonomy
+ end
+
+ def update # rubocop:disable Metrics/AbcSize
+ successful = @taxon.transaction do
+ parent_id = params[:taxon][:parent_id]
+ set_position
+ assign_parent(parent_id)
+
+ @taxon.save!
+
+ # regenerate permalink
+ regenerate_permalink if parent_id
+
+ set_permalink_params
+
+ # check if we need to rename child taxons if parent name or permalink changes
+ @update_children = true if params[:taxon][:name] != @taxon.name || params[:taxon][:permalink] != @taxon.permalink
+
+ @taxon.create_icon(attachment: taxon_params[:icon]) if taxon_params[:icon]
+ @taxon.update(taxon_params.except(:icon))
+ end
+ if successful
+ flash[:success] = flash_message_for(@taxon, :successfully_updated)
+
+ # rename child taxons
+ rename_child_taxons if @update_children
+
+ respond_with(@taxon) do |format|
+ format.html { redirect_to spree.edit_transit_place_url(params[:id]) }
+ format.json { render json: @taxon.to_json }
+ end
+ else
+ respond_with(@taxon) do |format|
+ format.html { render :edit, status: :unprocessable_entity }
+ format.json { render json: @taxon.errors.full_messages.to_sentence, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ def location_after_save
+ spree.transit_places_url
+ end
+
+ def set_permalink_part
+ @permalink_part = @object.permalink.split('/').last
+ @parent_permalink = @object.permalink.split('/')[0...-1].join('/')
+ @parent_permalink += '/' if @parent_permalink.present?
+ end
+
+ def set_permalink_params
+ set_permalink_part
+
+ return unless params.key? 'permalink_part'
+
+ params[:taxon][:permalink] = @parent_permalink + params[:permalink_part]
+ end
+
+ def taxon_params
+ params.require(:taxon).permit(:stop, :station, :branch, :location, permitted_taxon_attributes)
+ end
+
+ def set_position
+ new_position = params[:taxon][:position]
+ @taxon.child_index = new_position.to_i if new_position
+ end
+
+ # If this method is applied to the model, it will set stop, location, and
+ # taxon attributes to 0 when a drag-and-drop action changes the position.
+ def set_data_type
+ place_types = params[:taxon]
+ if place_types[:stop].to_i.zero? && place_types[:station].to_i.zero? && place_types[:branch].to_i.zero?
+ # If not checked, it will set the location instead.
+ place_types[:location] = 1
+ @taxon[:data_type] = (place_types[:location].to_i * (2**3))
+ end
+
+ result = (place_types[:stop].to_i * (2**0)) + (place_types[:station].to_i * (2**1)) + (place_types[:branch].to_i * (2**2)) + (place_types[:location].to_i * (2**3)) # rubocop:disable Layout/LineLength
+ @taxon[:data_type] = result
+ end
+
+ def assign_parent(parent_id)
+ @taxon.parent = current_store.taxons.find(parent_id) if parent_id
+ end
+
+ def parent_data
+ if action_name == 'index'
+ nil
+ else
+ super
+ end
+ end
+
+ def rename_child_taxons
+ @taxon.descendants.each do |taxon|
+ reload_taxon_and_set_permalink(taxon)
+ end
+ end
+
+ def regenerate_permalink
+ reload_taxon_and_set_permalink(@taxon)
+ @update_children = true
+ end
+
+ def reload_taxon_and_set_permalink(taxon)
+ taxon.reload
+ taxon.set_permalink
+ taxon.save!
+ end
+
+ def collection_url
+ spree.polymorphic_url([:transit, place.to_sym], options)
+ end
+
+ def edit_transit_taxon_path(taxon)
+ spree.edit_transit_taxonomy_taxon_path(taxon.taxonomy, taxon.id)
+ end
+
+ def scope
+ current_store.taxonomies
+ end
+
+ def model_class
+ Spree::Taxon
+ end
+
+ def object_name
+ 'taxon'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/reservations_controller.rb b/app/controllers/spree/transit/reservations_controller.rb
new file mode 100644
index 000000000..7cd03c0c0
--- /dev/null
+++ b/app/controllers/spree/transit/reservations_controller.rb
@@ -0,0 +1,15 @@
+module Spree
+ module Transit
+ class ReservationsController < Spree::Transit::BaseController
+ def index; end
+
+ def model_class
+ Spree::Order
+ end
+
+ def object_name
+ 'order'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/routes_controller.rb b/app/controllers/spree/transit/routes_controller.rb
new file mode 100644
index 000000000..89e08741e
--- /dev/null
+++ b/app/controllers/spree/transit/routes_controller.rb
@@ -0,0 +1,66 @@
+module Spree
+ module Transit
+ class RoutesController < Spree::Transit::BaseController
+ before_action :load_data
+
+ def scope
+ current_vendor.products.where(product_type: :transit)
+ end
+
+ def show
+ redirect_to spree.edit_transit_route_path(@object)
+ end
+
+ def load_data
+ @product ||= model_class.find_by(id: params[:id])
+ @option_types = OptionType.order(:name)
+ @shipping_categories = ShippingCategory.order(:name)
+ @selected_option_type_ids = Spree::OptionType.where(name: %w[origin destination]).ids
+ @taxons = Spree::Taxonomy.place.taxons
+ end
+
+ def new
+ @object.product_type = :transit
+ end
+
+ def collection
+ return @collection if defined?(@collection)
+
+ params[:q] ||= {}
+ params[:q][:deleted_at_null] ||= '1'
+
+ params[:q][:s] ||= 'updated_at desc'
+
+ current_vendor.products.where(product_type: :transit)
+
+ @search = current_vendor.products.where(product_type: :transit).ransack(params[:q].reject { |k, _v| k.to_s == 'deleted_at_null' })
+
+ @collection = @search.result
+ .page(params[:page])
+ .per(params[:per_page] || Spree::Backend::Config[:variants_per_page])
+ @collection
+ end
+
+ # overrided
+ def find_resource
+ product_scope.with_deleted.friendly.find(params[:id])
+ end
+
+ def product_scope
+ current_store.products.accessible_by(current_ability, :index)
+ end
+
+ def location_after_save
+ transit_routes_url
+ end
+
+ def model_class
+ Spree::Product
+ end
+
+ def object_name
+ 'product'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/service_calendars_controller.rb b/app/controllers/spree/transit/service_calendars_controller.rb
new file mode 100644
index 000000000..e9b505d22
--- /dev/null
+++ b/app/controllers/spree/transit/service_calendars_controller.rb
@@ -0,0 +1,109 @@
+module Spree
+ module Transit
+ class ServiceCalendarsController < Spree::Transit::BaseController
+ before_action :load_vendor
+ create.before :set_vendor
+ update.before :set_vendor
+
+ new_action.before :set_exception_rules
+
+ helper 'spree_cm_commissioner/transit/service_calendars'
+
+ def update_status
+ if @object.update(active: !@object.active)
+ flash[:success] = flash_message_for(@object, :successfully_updated)
+ else
+ flash[:error] = @object.errors.full_messages.to_sentence
+ end
+
+ redirect_to transit_vendor_service_calendars_url
+ rescue ActiveRecord::RecordInvalid => e
+ flash[:error] = e.message
+ redirect_to transit_vendor_service_calendars_url
+ end
+
+ def location_after_save
+ transit_vendor_service_calendars_url
+ end
+
+ protected
+
+ def collection_url(options = {})
+ transit_vendor_service_calendars_url(options)
+ end
+
+ def permitted_resource_params
+ add_exception_rules_type(params[:spree_cm_commissioner_service_calendar][:exception_rules])
+ service_calendar_params = params.require(:spree_cm_commissioner_service_calendar)
+ .permit(:calendarable_id, :calendarable_type, :start_date, :end_date,
+ :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday, :name, :not_available_reason,
+ :service_type
+ )
+ service_calendar_params[:exception_rules] = build_exception_rules(params[:spree_cm_commissioner_service_calendar][:exception_rules])
+ service_calendar_params
+ end
+
+ def model_class
+ SpreeCmCommissioner::ServiceCalendar
+ end
+
+ def object_name
+ 'spree_cm_commissioner_service_calendars'
+ end
+
+ private
+
+ def build_exception_rules(exception_rules)
+ exception_rules.values.reject! { |rule| rule['from'].blank? || rule['to'].blank? } || exception_rules.values
+ end
+
+ def add_exception_rules_type(exception_rules)
+ exception_rules.values.each do |rule|
+ rule['type'] = 'exclusion'
+ end
+ end
+
+ def set_vendor
+ @object.calendarable_type = calendarable_type
+ @object.calendarable_id = calendarable_id
+ end
+
+ def load_vendor
+ @vendor ||= vendors.find { |v| v[:slug] == session[:transit_current_vendor_slug] } || vendors.first
+ end
+
+ def collection
+ load_vendor
+
+ @objects = model_class.where(
+ calendarable_type: calendarable_type,
+ calendarable_id: calendarable_id
+ ).order(id: :desc)
+
+ @search = @objects.ransack(params[:q])
+ @collection = @search.result
+
+ @collection
+ end
+
+ def set_exception_rules
+ @exception_rules = [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }]
+ end
+
+ def build_resource
+ today = Time.zone.today
+ model_class.new(start_date: today,
+ exception_rules: [{ from: nil, to: nil }]
+ )
+ end
+
+ def calendarable_id
+ @vendor.id
+ end
+
+ def calendarable_type
+ @vendor.class.name
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/stops_controller.rb b/app/controllers/spree/transit/stops_controller.rb
new file mode 100644
index 000000000..4c932ebe9
--- /dev/null
+++ b/app/controllers/spree/transit/stops_controller.rb
@@ -0,0 +1,32 @@
+module Spree
+ module Transit
+ class StopsController < Spree::Transit::BaseController
+ before_action :load_location
+
+ def collection
+ return @collection if defined?(@collection)
+
+ current_vendor.stops
+
+ @search = current_vendor.stops.includes(:state).ransack(params[:q])
+ @collection = @search.result
+ end
+
+ def load_location
+ @locations = Spree::State.all
+ end
+
+ def location_after_save
+ transit_stops_url
+ end
+
+ def model_class
+ SpreeCmCommissioner::Stop
+ end
+
+ def object_name
+ 'spree_cm_commissioner_stop'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/trip_stops_controller.rb b/app/controllers/spree/transit/trip_stops_controller.rb
new file mode 100644
index 000000000..52712f2a9
--- /dev/null
+++ b/app/controllers/spree/transit/trip_stops_controller.rb
@@ -0,0 +1,35 @@
+module Spree
+ module Transit
+ class TripStopsController < Spree::Transit::BaseController
+ before_action :load_data
+ def index; end
+
+ def load_data
+ @product = Spree::Product.find_by(slug: params[:route_id])
+ @trip = SpreeCmCommissioner::Trip.find_by(id: params[:trip_id]) if params[:trip_id]
+ @trip_stops = @trip.trip_stops.sort_by(&:sequence)
+ end
+
+ def update_sequences
+ ApplicationRecord.transaction do
+ params[:positions].each do |id, index|
+ SpreeCmCommissioner::TripStop.where(id: id).update(sequence: index)
+ end
+ end
+
+ respond_to do |format|
+ format.html { redirect_to transit_route_trip_trip_stops_path(@product, @product.trip) }
+ format.js { render plain: 'Ok' }
+ end
+ end
+
+ def object_name
+ 'spree_cm_commissioner_trip_stop'
+ end
+
+ def model_class
+ SpreeCmCommissioner::TripStop
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/trips_controller.rb b/app/controllers/spree/transit/trips_controller.rb
new file mode 100644
index 000000000..9b99359f7
--- /dev/null
+++ b/app/controllers/spree/transit/trips_controller.rb
@@ -0,0 +1,90 @@
+module Spree
+ module Transit
+ class TripsController < Spree::Transit::BaseController
+ before_action :load_data
+
+ def load_data
+ @product = Spree::Product.find_by(slug: params[:route_id])
+ @vehicles = SpreeCmCommissioner::Vehicle.includes(:vehicle_type).order('cm_vehicle_types.name')
+ @stops = Spree::Taxonomy.place.taxons.select(&:stop?).to_a
+ @places = Spree::Taxonomy.place.taxons.select(&:location?).to_a
+ @trip = SpreeCmCommissioner::Trip.find_by(id: params[:id]) if params[:id]
+ end
+
+ def model_class
+ SpreeCmCommissioner::Trip
+ end
+
+ def edit
+ @trip_stops = @trip.trip_stops.sort_by(&:sequence)
+ @boarding_stops = @trip.trip_stops.boarding.to_a.pluck(:stop_id)
+ @drop_off_stops = @trip.trip_stops.drop_off.to_a.pluck(:stop_id)
+ end
+
+ def create
+ @trip = model_class.new
+ if @trip.update(trip_params(nested_stop_attributes))
+ flash[:success] = flash_message_for(@trip, :successfully_created)
+ redirect_to edit_transit_route_trip_path(@product, @product.trip.id)
+ else
+ flash[:error] = "create failed. Errors: #{@trip.errors.full_messages.join(', ')}"
+ redirect_back(fallback_location: new_transit_route_trip_path(@product))
+ end
+ end
+
+ def update
+ if @trip.update(trip_params(nested_stop_attributes))
+ flash[:success] = flash_message_for(@trip, :successfully_updated)
+ else
+ flash[:error] = "update failed. Errors: #{@trip.errors.full_messages.join(', ')}"
+ end
+ redirect_back(fallback_location: edit_transit_route_trip_path(@product, @trip))
+ end
+
+ def nested_stop_attributes
+ cm_params = params.require('spree_cm_commissioner_trip')
+ trip_stops = @trip.trip_stops.index_by(&:stop_id)
+
+ boarding_points_attributes = (cm_params.delete('boarding_points') || []).compact_blank
+ .map do |point_id|
+ trip_stop = trip_stops.delete(point_id.to_i)
+ { stop_id: point_id, stop_type: 'boarding', id: trip_stop.try(:id) }
+ end
+
+ drop_off_points_attributes = (cm_params.delete('drop_off_points') || []).compact_blank
+ .map do |point_id|
+ trip_stop = trip_stops.delete(point_id.to_i)
+ { stop_id: point_id, stop_type: 'drop_off', id: trip_stop.try(:id) }
+ end
+
+ trip_stops.each_value do |trip_stop|
+ next unless trip_stop.stop_id != @trip.origin_id && trip_stop.stop_id != @trip.destination_id
+
+ boarding_points_attributes << { id: trip_stop.id,
+ _destroy: '1'
+ }
+ end
+
+ cm_params[:trip_stops_attributes] = boarding_points_attributes + drop_off_points_attributes
+ cm_params
+ end
+
+ def trip_params(params)
+ params.permit(
+ :origin_id, :destination_id, :vehicle_id, :hours, :minutes, :seconds,
+ 'departure_time(1i)', 'departure_time(2i)', 'departure_time(3i)',
+ 'departure_time(4i)', 'departure_time(5i)', :product_id,
+ trip_stops_attributes: %i[stop_id stop_type _destroy id]
+ )
+ end
+
+ def object_name
+ 'spree_cm_commissioner_trip'
+ end
+
+ def location_after_save
+ transit_routes_path
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/vectors/amenity_values_controller.rb b/app/controllers/spree/transit/vectors/amenity_values_controller.rb
new file mode 100644
index 000000000..4afe33c76
--- /dev/null
+++ b/app/controllers/spree/transit/vectors/amenity_values_controller.rb
@@ -0,0 +1,40 @@
+module Spree
+ module Transit
+ module Vectors
+ class AmenityValuesController < Spree::Transit::BaseController
+ skip_before_action :load_resource, only: :update
+ before_action :load_object, only: :update
+
+ def load_object
+ option_value_id = params['option-value-id']
+ @object = Spree::OptionValue.find(option_value_id)
+ end
+
+ # @overrided
+ def collection
+ @vector_icons = SpreeCmCommissioner::VectorIcon.all
+ @collection ||= Spree::OptionType.where(kind: kind)
+ end
+
+ # @overrided
+ def model_class
+ Spree::OptionValue
+ end
+
+ # @overrided
+ def object_name
+ 'option_value'
+ end
+
+ # @overrided
+ def collection_url(options = { kind: kind })
+ transit_vectors_amenity_values_url(options)
+ end
+
+ def kind
+ @kind = 'vehicle_type'
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/vectors/icons_controller.rb b/app/controllers/spree/transit/vectors/icons_controller.rb
new file mode 100644
index 000000000..d9d1f6027
--- /dev/null
+++ b/app/controllers/spree/transit/vectors/icons_controller.rb
@@ -0,0 +1,26 @@
+module Spree
+ module Transit
+ module Vectors
+ class IconsController < Spree::Transit::BaseController
+ # @overrided
+ def collection
+ @collection ||= searcher.page(params[:page]).per(params[:per_page])
+ end
+
+ def model_class
+ SpreeCmCommissioner::VectorIcon
+ end
+
+ private
+
+ def searcher
+ model_class.search(
+ path_prefix: params[:path_prefix],
+ extension: params[:extension],
+ query: params[:query]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/vehicle_photos_controller.rb b/app/controllers/spree/transit/vehicle_photos_controller.rb
new file mode 100644
index 000000000..39f2cf12e
--- /dev/null
+++ b/app/controllers/spree/transit/vehicle_photos_controller.rb
@@ -0,0 +1,53 @@
+module Spree
+ module Transit
+ class VehiclePhotosController < Spree::Transit::BaseController
+ prepend_before_action :load_vehicle
+
+ create.before :set_viewable
+ update.before :set_viewable
+
+ def index
+ end
+
+ def load_vehicle
+ @vehicle ||= current_vendor.vehicles.find(params[:vehicle_id])
+ end
+
+ def set_viewable
+ @object.viewable_type = viewable_type
+ @object.viewable_id = viewable_id
+ end
+
+ def viewable_type
+ @vehicle.class.name
+ end
+
+ def viewable_id
+ @vehicle.id
+ end
+
+ def collection
+ @objects = model_class.where(
+ viewable_type: viewable_type,
+ viewable_id: viewable_id
+ )
+ end
+
+ def location_after_save
+ transit_vehicle_vehicle_photos_url
+ end
+
+ def model_class
+ SpreeCmCommissioner::VehiclePhoto
+ end
+
+ def object_name
+ 'spree_cm_commissioner_vehicle_photo'
+ end
+
+ def collection_url(options = {})
+ transit_vehicle_vehicle_photos_url(options)
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/spree/transit/vehicle_seats_controller.rb b/app/controllers/spree/transit/vehicle_seats_controller.rb
new file mode 100644
index 000000000..da1c7a201
--- /dev/null
+++ b/app/controllers/spree/transit/vehicle_seats_controller.rb
@@ -0,0 +1,29 @@
+module Spree
+ module Transit
+ class VehicleSeatsController < Spree::Transit::BaseController
+ def load_seat
+ @seats = JSON.parse(params[:seats]).to_a
+ render :partial => 'spree/transit/vehicle_seats/seats'
+ end
+
+ def create
+ @seats = params[:seats]
+
+ @flatten_seats = JSON.parse(@seats).flatten
+ @flatten_seats.each do |seat|
+ SpreeCmCommissioner::VehicleSeat.create(seat)
+ end
+ end
+
+ private
+
+ def model_class
+ SpreeCmCommissioner::VehicleSeat
+ end
+
+ def object_name
+ 'spree_cm_commissioner_vehicle_seat'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/vehicle_types_controller.rb b/app/controllers/spree/transit/vehicle_types_controller.rb
new file mode 100644
index 000000000..aadc6b9bc
--- /dev/null
+++ b/app/controllers/spree/transit/vehicle_types_controller.rb
@@ -0,0 +1,81 @@
+module Spree
+ module Transit
+ class VehicleTypesController < Spree::Transit::BaseController
+ before_action :load_amenities, except: %i[index layer]
+ before_action :load_status, except: %i[index layer]
+
+ def index
+ respond_with(@collection)
+ end
+
+ def location_after_save
+ edit_transit_vehicle_type_path(@object)
+ end
+
+ def layer
+ @seats = JSON.parse(params[:seats]).to_a
+ @row = params[:row]
+ @column = params[:column]
+ @layer_name = params[:layer_name]
+ @created_at = params[:created_at]
+ render :partial => 'spree/transit/vehicle_types/seat_view'
+ end
+
+ def load_amenities
+ @amenities = Spree::OptionType.amenities.option_values.pluck(:name, :id)
+ @selected_option_value_ids = @object.option_values.pluck(:id)
+ end
+
+ def load_status
+ @statuses = SpreeCmCommissioner::VehicleType.state_machine.states.map(&:name)
+ end
+
+ def new
+ @vehicle_type = SpreeCmCommissioner::VehicleType.new
+ end
+
+ def scope
+ @vehicle_types = current_vendor.vehicle_types
+ end
+
+ def collection
+ return @collection if defined?(@collection)
+
+ scope
+
+ @search = scope.ransack(params[:q])
+ @collection = @search.result
+ end
+
+ def edit
+ @seats = @object.seat_layers
+ end
+
+ # overrided
+ def permitted_resource_params
+ vehicle_type_params = params[:spree_cm_commissioner_vehicle_type]
+ selected_option_value_ids = vehicle_type_params[:option_value_ids]
+
+ option_values = Spree::OptionValue.where(id: selected_option_value_ids)
+ { option_values: option_values,
+ name: vehicle_type_params[:name],
+ code: vehicle_type_params[:code],
+ vendor_id: vehicle_type_params[:vendor_id],
+ route_type: vehicle_type_params[:route_type],
+ status: vehicle_type_params[:status],
+ allow_seat_selection: vehicle_type_params[:allow_seat_selection],
+ vehicle_seats_count: vehicle_type_params[:vehicle_seats_count]
+ }
+ end
+
+ # @overrided
+ def model_class
+ SpreeCmCommissioner::VehicleType
+ end
+
+ def object_name
+ 'spree_cm_commissioner_vehicle_type'
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/vehicles_controller.rb b/app/controllers/spree/transit/vehicles_controller.rb
new file mode 100644
index 000000000..4b68da166
--- /dev/null
+++ b/app/controllers/spree/transit/vehicles_controller.rb
@@ -0,0 +1,54 @@
+module Spree
+ module Transit
+ class VehiclesController < Spree::Transit::BaseController
+ before_action :set_vendor, if: -> { member_action? }
+ before_action :load_vehicle_types
+
+ def new
+ @vehicle = SpreeCmCommissioner::Vehicle.new
+ super
+ end
+
+ def edit
+ @vehicle = current_vendor.vehicles.find(params[:id])
+ end
+
+ def load_vehicle_types
+ @vehicle_types = current_vendor.vehicle_types
+ end
+
+ def scope
+ @vehicles = current_vendor.vehicles
+ end
+
+ def collection
+ return @collection if defined?(@collection)
+
+ scope
+
+ @search = scope.includes(:vehicle_type).ransack(params[:q])
+ @collection = @search.result
+ end
+
+ def location_after_save
+ transit_vehicles_url
+ end
+
+ def model_class
+ SpreeCmCommissioner::Vehicle
+ end
+
+ def object_name
+ 'spree_cm_commissioner_vehicle'
+ end
+
+ def vehicle_params
+ params.require(:vehicle).permit(:code, :license_plate)
+ end
+
+ def set_vendor
+ permitted_resource_params[:vendor] = current_vendor
+ end
+ end
+ end
+end
diff --git a/app/controllers/spree/transit/vendors_controller.rb b/app/controllers/spree/transit/vendors_controller.rb
new file mode 100644
index 000000000..792ea1cb3
--- /dev/null
+++ b/app/controllers/spree/transit/vendors_controller.rb
@@ -0,0 +1,83 @@
+module Spree
+ module Transit
+ class VendorsController < Spree::Transit::BaseController
+ before_action :build_logo, only: %i[create update]
+ before_action :build_payment_qrcode, only: %i[create update]
+
+ def create
+ @vendor.build_image(attachment: permitted_resource_params.delete(:image)) if permitted_resource_params[:image]
+ super
+ end
+
+ def update
+ @vendor.create_image(attachment: permitted_resource_params.delete(:image)) if permitted_resource_params[:image]
+ format_translations if defined? SpreeGlobalize
+ super
+ end
+
+ def update_positions
+ params[:positions].each do |id, position|
+ vendor = Spree::Vendor.find(id)
+ vendor.set_list_position(position)
+ end
+
+ respond_to do |format|
+ format.js { render plain: 'Ok' }
+ end
+ end
+
+ private
+
+ def find_resource
+ Vendor.with_deleted.friendly.find(params[:id])
+ end
+
+ def collection
+ params[:q] = {} if params[:q].blank?
+ vendors = super.order(priority: :asc)
+ @search = vendors.ransack(params[:q])
+
+ @collection = @search.result
+ .includes(vendor_includes)
+ .page(params[:page])
+ .per(params[:per_page])
+ end
+
+ def vendor_includes
+ {
+ image: [],
+ products: []
+ }
+ end
+
+ def format_translations
+ return if params[:vendor][:translations_attributes].blank?
+
+ params[:vendor][:translations_attributes].each do |_, data|
+ translation = @vendor.translations.find_or_create_by(locale: data[:locale])
+ translation.name = data[:name]
+ translation.about_us = data[:about_us]
+ translation.contact_us = data[:contact_us]
+ translation.slug = data[:slug]
+ translation.save!
+ end
+ end
+
+ def build_logo
+ return unless permitted_resource_params[:logo]
+
+ @vendor.build_logo(attachment: permitted_resource_params.delete(:logo))
+ end
+
+ def build_payment_qrcode
+ return unless permitted_resource_params[:payment_qrcode]
+
+ @vendor.build_payment_qrcode(attachment: permitted_resource_params.delete(:payment_qrcode))
+ end
+
+ def location_after_save
+ edit_transit_vendor_url(@current_vendor.id)
+ end
+ end
+ end
+end
diff --git a/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb b/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb
new file mode 100644
index 000000000..e98029f7f
--- /dev/null
+++ b/app/finders/spree_cm_commissioner/line_items/find_by_variant_decorator.rb
@@ -0,0 +1,20 @@
+module SpreeCmCommissioner
+ module LineItems
+ module FindByVariantDecorator
+ # ovveride
+ def execute(order:, variant:, options: {})
+ return super unless variant.product.product_type == 'transit'
+
+ order.line_items.detect do |line_item|
+ next if options[:date].present? && !(line_item.variant_id == variant.id && line_item.date == options[:date])
+
+ Spree::Dependencies.cart_compare_line_items_service.constantize.call(order: order, line_item: line_item, options: options).value
+ end
+ end
+ end
+ end
+end
+
+unless Spree::LineItems::FindByVariant.included_modules.include?(SpreeCmCommissioner::LineItems::FindByVariantDecorator)
+ Spree::LineItems::FindByVariant.prepend(SpreeCmCommissioner::LineItems::FindByVariantDecorator)
+end
diff --git a/app/helpers/spree/transit/navigation_helper_decorator.rb b/app/helpers/spree/transit/navigation_helper_decorator.rb
new file mode 100644
index 000000000..bf725867f
--- /dev/null
+++ b/app/helpers/spree/transit/navigation_helper_decorator.rb
@@ -0,0 +1,14 @@
+module Spree
+ module Transit
+ module NavigationHelperDecorator
+ def transit_main_menu_tree(text, icon: nil, sub_menu: nil, url: '#')
+ content_tag :li, class: 'sidebar-menu-item d-block w-100 text-muted' do
+ main_menu_item(text, url: url, icon: icon) +
+ render(partial: "spree/transit/shared/sub_menu/#{sub_menu}")
+ end
+ end
+ end
+ end
+end
+
+Spree::Admin::NavigationHelper.prepend(Spree::Transit::NavigationHelperDecorator)
diff --git a/app/helpers/spree/transit/sortable_tree_helper.rb b/app/helpers/spree/transit/sortable_tree_helper.rb
new file mode 100644
index 000000000..06919231e
--- /dev/null
+++ b/app/helpers/spree/transit/sortable_tree_helper.rb
@@ -0,0 +1,32 @@
+module Spree
+ module Transit
+ module SortableTreeHelper
+ def sortable_tree_bar(parent_resource, child_resource)
+ partial_name = parent_resource.class.name.demodulize.underscore
+ render "spree/transit/shared/sortable_tree/#{partial_name}", parent_resource: parent_resource, child_resource: child_resource
+ end
+
+ def build_sortable_tree(parent_resource, child_resource)
+ child_resource_name = child_resource.class.name.demodulize.underscore
+ descendants = []
+
+ unless child_resource.leaf?
+ child_resource.children.each do |child_item|
+ descendants << build_sortable_tree(parent_resource, child_item) unless child_resource.leaf?
+ end
+ end
+
+ row = sortable_tree_bar(parent_resource, child_resource)
+ container = content_tag(:div, raw(descendants.join), data: { sortable_tree_parent_id_value: child_resource.id })
+
+ content_tag(:div, row + container,
+ class: 'sortable-tree-item draggable removable-dom-element',
+ data: {
+ sortable_tree_resource_name_value: child_resource_name.singularize,
+ sortable_tree_update_url_value: "/api/v2/platform/#{child_resource_name.pluralize}/#{child_resource.id}/reposition"
+ }
+ )
+ end
+ end
+ end
+end
diff --git a/app/helpers/spree_cm_commissioner/transit/service_calendars_helper.rb b/app/helpers/spree_cm_commissioner/transit/service_calendars_helper.rb
new file mode 100644
index 000000000..c8695e48e
--- /dev/null
+++ b/app/helpers/spree_cm_commissioner/transit/service_calendars_helper.rb
@@ -0,0 +1,17 @@
+module SpreeCmCommissioner
+ module Transit
+ module ServiceCalendarsHelper
+ def toggle_status_btn(resource)
+ label = resource.active ? 'Active' : 'Disabled'
+ btn_active_class = resource.active ? 'btn-primary' : 'btn-warning'
+
+ button_to(
+ label,
+ update_status_transit_vendor_service_calendar_url(resource.id, resource),
+ form: { data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-active #{btn_active_class}" },
+ method: :patch
+ )
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/spree_cm_commissioner/product_type.rb b/app/models/concerns/spree_cm_commissioner/product_type.rb
index 5cfe30c96..fcfa560d4 100644
--- a/app/models/concerns/spree_cm_commissioner/product_type.rb
+++ b/app/models/concerns/spree_cm_commissioner/product_type.rb
@@ -5,7 +5,7 @@ module SpreeCmCommissioner
module ProductType
extend ActiveSupport::Concern
- PRODUCT_TYPES = %i[accommodation service ecommerce].freeze
+ PRODUCT_TYPES = %i[accommodation service ecommerce transit].freeze
included do
enum product_type: PRODUCT_TYPES if table_exists? && column_names.include?('product_type')
diff --git a/app/models/concerns/spree_cm_commissioner/route_type.rb b/app/models/concerns/spree_cm_commissioner/route_type.rb
new file mode 100644
index 000000000..caff4a77b
--- /dev/null
+++ b/app/models/concerns/spree_cm_commissioner/route_type.rb
@@ -0,0 +1,11 @@
+module SpreeCmCommissioner
+ module RouteType
+ extend ActiveSupport::Concern
+
+ ROUTE_TYPES = %i[automobile subway rails ferry].freeze
+
+ included do
+ enum route_type: ROUTE_TYPES if table_exists? && column_names.include?('route_type')
+ end
+ end
+end
diff --git a/app/models/concerns/spree_cm_commissioner/service_calendar_type.rb b/app/models/concerns/spree_cm_commissioner/service_calendar_type.rb
new file mode 100644
index 000000000..51e08f96b
--- /dev/null
+++ b/app/models/concerns/spree_cm_commissioner/service_calendar_type.rb
@@ -0,0 +1,11 @@
+module SpreeCmCommissioner
+ module ServiceCalendarType
+ extend ActiveSupport::Concern
+
+ SERVICE_TYPES = %i[unavailable available].freeze
+
+ included do
+ enum service_type: SERVICE_TYPES
+ end
+ end
+end
diff --git a/app/models/concerns/spree_cm_commissioner/taxonomy_kind.rb b/app/models/concerns/spree_cm_commissioner/taxonomy_kind.rb
new file mode 100644
index 000000000..aeaeb27ae
--- /dev/null
+++ b/app/models/concerns/spree_cm_commissioner/taxonomy_kind.rb
@@ -0,0 +1,11 @@
+module SpreeCmCommissioner
+ module TaxonomyKind
+ extend ActiveSupport::Concern
+
+ KINDS = %i[cms category transit].freeze
+
+ included do
+ enum kind: KINDS if table_exists? && column_names.include?('kind')
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/branch.rb b/app/models/spree_cm_commissioner/branch.rb
new file mode 100644
index 000000000..dfc70e012
--- /dev/null
+++ b/app/models/spree_cm_commissioner/branch.rb
@@ -0,0 +1,12 @@
+require_dependency 'spree_cm_commissioner'
+
+module SpreeCmCommissioner
+ class Branch < SpreeCmCommissioner::Place
+ belongs_to :state, class_name: 'Spree::State', optional: true
+ belongs_to :vendor, class_name: 'Spree::Vendor'
+
+ def validate_reference?
+ false
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/line_item_decorator.rb b/app/models/spree_cm_commissioner/line_item_decorator.rb
index 22d1f9943..b1e19bbdb 100644
--- a/app/models/spree_cm_commissioner/line_item_decorator.rb
+++ b/app/models/spree_cm_commissioner/line_item_decorator.rb
@@ -3,11 +3,16 @@ module LineItemDecorator
def self.prepended(base)
base.include SpreeCmCommissioner::LineItemDurationable
base.delegate :need_confirmation, to: :product
-
base.belongs_to :accepted_by, class_name: 'Spree::User', optional: true
base.belongs_to :rejected_by, class_name: 'Spree::User', optional: true
+ base.has_many :line_item_seats, class_name: 'SpreeCmCommissioner::LineItemSeat', dependent: :destroy
+
+ base.accepts_nested_attributes_for :line_item_seats, allow_destroy: true
base.before_save :update_vendor_id
+ base.before_save :update_quantity, if: :transit?
+
+ base.validate :validate_seats_reservation, if: :transit?
base.delegate :product_type, :accommodation?, :service?, :ecommerce?, to: :product
base.before_create :add_due_date, if: :subscription?
@@ -15,6 +20,8 @@ def self.prepended(base)
base.whitelisted_ransackable_attributes |= %w[to_date from_date]
end
+ delegate :transit?, to: :variant
+
def reservation?
date_present? && !subscription?
end
@@ -35,8 +42,27 @@ def amount
end
end
+ # override
+ def sufficient_stock?
+ return super unless variant.product.product_type == 'transit'
+
+ transit_sufficient_stock?
+ end
+
private
+ def transit_sufficient_stock?
+ return selected_seats_available? if reservation_trip.allow_seat_selection
+
+ seat_quantity_available?(reservation_trip)
+ end
+
+ def update_quantity
+ return if line_item_seats.blank?
+
+ self.quantity = line_item_seats.size
+ end
+
def update_vendor_id
self.vendor_id = variant.vendor_id
end
@@ -69,6 +95,45 @@ def due_days
end
from_date + day.days
end
+
+ def validate_seats_reservation
+ if reservation_trip.allow_seat_selection && !selected_seats_available?
+ errors.add(:base, :some_seats_are_booked, message: 'Some seats are already booked')
+ elsif !reservation_trip.allow_seat_selection && !seat_quantity_available?(reservation_trip)
+ errors.add(:quantity, :exceeded_available_quantity, message: 'exceeded available quantity')
+ end
+ end
+
+ def selected_seats_available?
+ selected_seat_ids = line_item_seats.map(&:seat_id)
+ !selected_seat_ids_occupied?(selected_seat_ids)
+ end
+
+ def seat_quantity_available?(trip)
+ booked_quantity = Spree::LineItem.joins(:order)
+ .where(variant_id: variant_id, date: date, spree_orders: { state: 'complete' })
+ .where.not(spree_line_items: { id: id })
+ .sum(:quantity)
+ remaining_quantity = trip.vehicle.number_of_seats - booked_quantity
+ remaining_quantity >= quantity
+ end
+
+ def reservation_trip
+ return @trip if defined? @trip
+
+ route = Spree::Variant.find_by(id: variant_id).product
+ @trip = route.trip
+ end
+
+ def selected_seat_ids_occupied?(selected_seat_ids)
+ # check to see if there are any selected_ids exist in the line_item_seats and belongs to completed order
+ SpreeCmCommissioner::LineItemSeat.joins(line_item: :order)
+ .where(seat_id: selected_seat_ids, date: date, variant_id: variant_id, spree_orders: { state: 'complete',
+ canceled_at: nil
+}
+ )
+ .present?
+ end
end
end
diff --git a/app/models/spree_cm_commissioner/line_item_seat.rb b/app/models/spree_cm_commissioner/line_item_seat.rb
new file mode 100644
index 000000000..3e5953f9b
--- /dev/null
+++ b/app/models/spree_cm_commissioner/line_item_seat.rb
@@ -0,0 +1,10 @@
+require_dependency 'spree_cm_commissioner'
+module SpreeCmCommissioner
+ class LineItemSeat < SpreeCmCommissioner::Base
+ belongs_to :line_item, class_name: 'Spree::LineItem'
+ belongs_to :seat, class_name: 'SpreeCmCommissioner::VehicleSeat'
+ belongs_to :variant, class_name: 'Spree::Variant'
+
+ validates :date, presence: true
+ end
+end
diff --git a/app/models/spree_cm_commissioner/option_type_decorator.rb b/app/models/spree_cm_commissioner/option_type_decorator.rb
index 30db3c45b..a4eb4efd5 100644
--- a/app/models/spree_cm_commissioner/option_type_decorator.rb
+++ b/app/models/spree_cm_commissioner/option_type_decorator.rb
@@ -1,10 +1,10 @@
module SpreeCmCommissioner
module OptionTypeDecorator
- ATTRIBUTE_TYPES = %w[float integer string boolean date coordinate state_selection].freeze
+ ATTRIBUTE_TYPES = %w[float integer string boolean date coordinate state_selection amenity departure_time duration vehicle_id].freeze
def self.prepended(base)
base.include SpreeCmCommissioner::ParameterizeName
- base.enum kind: %i[variant product vendor]
+ base.enum kind: %i[variant product vendor vehicle_type transit]
base.validates :name, presence: true
base.validates :attr_type, inclusion: { in: ATTRIBUTE_TYPES }
@@ -13,6 +13,16 @@ def self.prepended(base)
base.scope :promoted, -> { where(promoted: true) }
base.whitelisted_ransackable_attributes = %w[kind]
+
+ def base.amenities
+ Spree::OptionType.where(kind: 'vehicle_type', name: 'amenities', presentation: 'Amenities', attr_type: 'amenity').first_or_create
+ end
+
+ def base.vehicle
+ Spree::OptionType.where(presentation: 'vehicle', attr_type: 'vehicle_id', kind: 'variant',
+ name: 'vehicle'
+ ).first_or_create
+ end
end
private
diff --git a/app/models/spree_cm_commissioner/option_value_vehicle_type.rb b/app/models/spree_cm_commissioner/option_value_vehicle_type.rb
new file mode 100644
index 000000000..1e4c568a6
--- /dev/null
+++ b/app/models/spree_cm_commissioner/option_value_vehicle_type.rb
@@ -0,0 +1,8 @@
+require_dependency 'spree_cm_commissioner'
+
+module SpreeCmCommissioner
+ class OptionValueVehicleType < ApplicationRecord
+ belongs_to :vehicle_type, class_name: 'SpreeCmCommissioner::VehicleType', dependent: :destroy
+ belongs_to :option_value, class_name: 'Spree::OptionValue', dependent: :destroy
+ end
+end
diff --git a/app/models/spree_cm_commissioner/place.rb b/app/models/spree_cm_commissioner/place.rb
index af7531b08..12c42f320 100644
--- a/app/models/spree_cm_commissioner/place.rb
+++ b/app/models/spree_cm_commissioner/place.rb
@@ -2,11 +2,23 @@
module SpreeCmCommissioner
class Place < ApplicationRecord
- validates :reference, presence: true
- validates :lat, presence: true
- validates :lon, presence: true
+ validates :reference, presence: true, if: :validate_reference?
+ validates :lat, presence: true, if: :validate_lat?
+ validates :lon, presence: true, if: :validate_lon?
has_many :nearby_places, class_name: 'SpreeCmCommissioner::VendorPlace', dependent: :destroy
has_many :vendors, through: :nearby_places, source: :vendor, class_name: 'Spree::Vendor'
+
+ def validate_reference?
+ true
+ end
+
+ def validate_lat?
+ true
+ end
+
+ def validate_lon?
+ true
+ end
end
end
diff --git a/app/models/spree_cm_commissioner/product_decorator.rb b/app/models/spree_cm_commissioner/product_decorator.rb
index 2fa030ccd..579f1ab6f 100644
--- a/app/models/spree_cm_commissioner/product_decorator.rb
+++ b/app/models/spree_cm_commissioner/product_decorator.rb
@@ -2,6 +2,7 @@ module SpreeCmCommissioner
module ProductDecorator
def self.prepended(base)
base.include SpreeCmCommissioner::ProductType
+ base.include SpreeCmCommissioner::RouteType
base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
through: :product_option_types, source: :option_type
@@ -18,6 +19,7 @@ def self.prepended(base)
}, source: :prices, through: :variants_including_master
base.has_one :default_state, through: :vendor
+ base.has_one :trip, class_name: 'SpreeCmCommissioner::Trip', dependent: :destroy
base.scope :min_price, lambda { |vendor|
joins(:prices_including_master)
@@ -31,6 +33,10 @@ def self.prepended(base)
.maximum('spree_prices.price').to_f
}
base.scope :subscribable, -> { where(subscribable: 1) }
+
+ base.whitelisted_ransackable_attributes |= %w[short_name route_type]
+
+ base.accepts_nested_attributes_for :trip, allow_destroy: true
end
end
end
diff --git a/app/models/spree_cm_commissioner/service_calendar.rb b/app/models/spree_cm_commissioner/service_calendar.rb
index bf66ea00a..859743c97 100644
--- a/app/models/spree_cm_commissioner/service_calendar.rb
+++ b/app/models/spree_cm_commissioner/service_calendar.rb
@@ -1,17 +1,18 @@
require_dependency 'spree_cm_commissioner'
module SpreeCmCommissioner
- class ServiceCalendar < ApplicationRecord
+ class ServiceCalendar < SpreeCmCommissioner::Base
# exception_rules = [
# { 'from' => Date.parse('2023-1-01'), 'to'=> Date.parse('2023-1-31'), 'type' => 'exclusion', 'reason' => 'Company retreat' }
# ]
# inclusion: Service has been added for the specified date (default).
# exclusion: Service has been removed for the specified date.
-
+ include SpreeCmCommissioner::ServiceCalendarType
EXCEPTION_RULE_JSON_SCHEMA = Pathname.new("#{COMMISSIONER_ROOT}/config/schemas/service_calendar_exception_rule.json")
- belongs_to :calendarable, polymorphic: true
+ belongs_to :calendarable, polymorphic: true, optional: false
validates :exception_rules, json: { schema: EXCEPTION_RULE_JSON_SCHEMA }
+ self.whitelisted_ransackable_attributes = %w[name]
## Callbacks
before_save :set_dates
diff --git a/app/models/spree_cm_commissioner/state_decorator.rb b/app/models/spree_cm_commissioner/state_decorator.rb
index ae6cfb480..b741f0a76 100644
--- a/app/models/spree_cm_commissioner/state_decorator.rb
+++ b/app/models/spree_cm_commissioner/state_decorator.rb
@@ -1,7 +1,9 @@
module SpreeCmCommissioner
module StateDecorator
def self.prepended(base)
+ base.whitelisted_ransackable_attributes |= %w[name abbr country_id]
base.has_many :vendors, foreign_key: 'default_state_id', class_name: 'Spree::Vendor', inverse_of: :default_state, dependent: :nullify
+ base.has_many :stops, class_name: 'SpreeCmCommissioner::Place'
def update_total_inventory
update(total_inventory: vendors.pluck(:total_inventory).sum)
diff --git a/app/models/spree_cm_commissioner/stop.rb b/app/models/spree_cm_commissioner/stop.rb
new file mode 100644
index 000000000..e75687cf6
--- /dev/null
+++ b/app/models/spree_cm_commissioner/stop.rb
@@ -0,0 +1,23 @@
+require_dependency 'spree_cm_commissioner'
+
+module SpreeCmCommissioner
+ class Stop < SpreeCmCommissioner::Place
+ belongs_to :branch, class_name: 'SpreeCmCommissioner::Branch'
+ belongs_to :state, class_name: 'Spree::State', optional: true
+ belongs_to :vendor, class_name: 'Spree::Vendor'
+
+ after_save :add_to_option_value
+
+ def validate_reference?
+ false
+ end
+
+ def add_to_option_value
+ origin = Spree::OptionType.find_by(attr_type: 'origin')&.id
+ destination = Spree::OptionType.find_by(attr_type: 'destination')&.id
+
+ Spree::OptionValue.create(option_type_id: origin, name: name, presentation: id)
+ Spree::OptionValue.create(option_type_id: destination, name: name, presentation: id)
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/taxon_decorator.rb b/app/models/spree_cm_commissioner/taxon_decorator.rb
index 9bea8b47c..703a07d37 100644
--- a/app/models/spree_cm_commissioner/taxon_decorator.rb
+++ b/app/models/spree_cm_commissioner/taxon_decorator.rb
@@ -1,6 +1,8 @@
module SpreeCmCommissioner
module TaxonDecorator
def self.prepended(base)
+ base.include SpreeCmCommissioner::Transit::TaxonBitwise
+
base.has_one :category_icon, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::TaxonCategoryIcon'
base.has_one :web_banner, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::TaxonWebBanner'
diff --git a/app/models/spree_cm_commissioner/taxonomy_decorator.rb b/app/models/spree_cm_commissioner/taxonomy_decorator.rb
new file mode 100644
index 000000000..27cdd38ec
--- /dev/null
+++ b/app/models/spree_cm_commissioner/taxonomy_decorator.rb
@@ -0,0 +1,17 @@
+module SpreeCmCommissioner
+ module TaxonomyDecorator
+ def self.prepended(base)
+ base.include SpreeCmCommissioner::TaxonomyKind
+
+ def base.place
+ ActiveRecord::Base.connected_to(role: :writing) do
+ Spree::Taxonomy.find_or_create_by(name: 'Place', kind: 'transit', store: Spree::Store.default)
+ end
+ end
+ end
+ end
+end
+
+unless Spree::Taxonomy.included_modules.include?(SpreeCmCommissioner::TaxonomyDecorator)
+ Spree::Taxonomy.prepend(SpreeCmCommissioner::TaxonomyDecorator)
+end
diff --git a/app/models/spree_cm_commissioner/trip.rb b/app/models/spree_cm_commissioner/trip.rb
new file mode 100644
index 000000000..714093b6b
--- /dev/null
+++ b/app/models/spree_cm_commissioner/trip.rb
@@ -0,0 +1,50 @@
+require_dependency 'spree_cm_commissioner'
+module SpreeCmCommissioner
+ class Trip < SpreeCmCommissioner::Base
+ attr_accessor :hours, :minutes, :seconds
+
+ before_validation :convert_duration_to_seconds
+
+ belongs_to :route, class_name: 'Spree::Product'
+ belongs_to :vehicle, class_name: 'SpreeCmCommissioner::Vehicle'
+ validates :departure_time, presence: true
+
+ validates :departure_time, presence: true
+ validates :duration, numericality: { greater_than: 0 }
+ validate :origin_and_destination_cannot_be_the_same
+
+ has_many :trip_stops, class_name: 'SpreeCmCommissioner::TripStop', dependent: :destroy
+
+ after_commit :create_trip_stops
+
+ accepts_nested_attributes_for :trip_stops, allow_destroy: true
+
+ def convert_duration_to_seconds
+ return if hours.blank? && minutes.blank? && seconds.blank?
+
+ self.duration = (hours.to_i * 3600) + (minutes.to_i * 60) + seconds.to_i
+ end
+
+ def create_trip_stops
+ trip_stops.create(stop_type: :boarding, stop_id: origin_id)
+ trip_stops.create(stop_type: :drop_off, stop_id: destination_id)
+ end
+
+ def duration_in_hms
+ return { hours: 0, minutes: 0, seconds: 0 } if duration.nil?
+
+ hours = duration / 3600
+ minutes = (duration % 3600) / 60
+ seconds = duration % 60
+ { hours: hours, minutes: minutes, seconds: seconds }
+ end
+
+ private
+
+ def origin_and_destination_cannot_be_the_same
+ return unless origin_id == destination_id
+
+ errors.add(:base, 'Origin and destination cannot be the same')
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/trip_stop.rb b/app/models/spree_cm_commissioner/trip_stop.rb
new file mode 100644
index 000000000..954d8f52d
--- /dev/null
+++ b/app/models/spree_cm_commissioner/trip_stop.rb
@@ -0,0 +1,29 @@
+require_dependency 'spree_cm_commissioner'
+module SpreeCmCommissioner
+ class TripStop < SpreeCmCommissioner::Base
+ acts_as_list column: :sequence, scope: :trip_id
+ enum stop_type: { boarding: 0, drop_off: 1 }
+
+ belongs_to :trip, class_name: 'SpreeCmCommissioner::Trip'
+ belongs_to :stop, class_name: 'Spree::Taxon'
+
+ before_validation :set_stop_name
+ after_create :create_vendor_stop
+
+ validates :stop_id, uniqueness: { scope: :trip_id }
+
+ def set_stop_name
+ self.stop_name = stop.name
+ end
+
+ def create_vendor_stop
+ vendor.vendor_stops.where(stop_id: stop_id, stop_type: stop_type).first_or_create
+ end
+
+ private
+
+ def vendor
+ Spree::Product.find(trip.product_id).vendor
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/variant_decorator.rb b/app/models/spree_cm_commissioner/variant_decorator.rb
index e822d1eb6..8946f5f83 100644
--- a/app/models/spree_cm_commissioner/variant_decorator.rb
+++ b/app/models/spree_cm_commissioner/variant_decorator.rb
@@ -19,6 +19,10 @@ def display_variant
"#{display_sku} - $#{display_price}"
end
+ def transit?
+ product.product_type == 'transit'
+ end
+
private
def update_vendor_price
diff --git a/app/models/spree_cm_commissioner/vehicle.rb b/app/models/spree_cm_commissioner/vehicle.rb
new file mode 100644
index 000000000..d9a5e5ecb
--- /dev/null
+++ b/app/models/spree_cm_commissioner/vehicle.rb
@@ -0,0 +1,34 @@
+require_dependency 'spree_cm_commissioner'
+
+module SpreeCmCommissioner
+ class Vehicle < SpreeCmCommissioner::Base
+ include SpreeCmCommissioner::RouteType
+
+ belongs_to :vehicle_type, class_name: 'SpreeCmCommissioner::VehicleType'
+ has_one :primary_photo, -> { order(position: :asc) }, class_name: 'SpreeCmCommissioner::VehiclePhoto', as: :viewable, dependent: :destroy
+ belongs_to :vendor, class_name: 'Spree::Vendor'
+
+ before_save :set_attributes
+ after_commit :create_vehicle_option_value
+
+ has_many :vehicle_photo, class_name: 'SpreeCmCommissioner::VehiclePhoto', as: :viewable, dependent: :destroy
+ has_many :vehicle_seats, class_name: 'SpreeCmCommissioner::VehicleSeat', through: :vehicle_type
+
+ validates :code, uniqueness: { scope: :vendor_id }, presence: true
+ validates :license_plate, uniqueness: {}, allow_blank: true
+ validates :vehicle_type, presence: true
+
+ def set_attributes
+ self.route_type = vehicle_type.route_type
+ self.number_of_seats = vehicle_type.vehicle_seats_count
+ self.allow_seat_selection = vehicle_type.allow_seat_selection
+ end
+
+ def create_vehicle_option_value
+ SpreeCmCommissioner::VehicleOptionValueCreator.call(self)
+ end
+
+ self.whitelisted_ransackable_attributes = %w[license_plate code]
+ self.whitelisted_ransackable_associations = %w[vehicle_type]
+ end
+end
diff --git a/app/models/spree_cm_commissioner/vehicle_photo.rb b/app/models/spree_cm_commissioner/vehicle_photo.rb
new file mode 100644
index 000000000..85f04d6f2
--- /dev/null
+++ b/app/models/spree_cm_commissioner/vehicle_photo.rb
@@ -0,0 +1,13 @@
+module SpreeCmCommissioner
+ class VehiclePhoto < Asset
+
+ # 16x9
+ def asset_styles
+ {
+ mini: '160x90>',
+ small: '480x270>',
+ medium: '960x540>'
+ }
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/models/spree_cm_commissioner/vehicle_seat.rb b/app/models/spree_cm_commissioner/vehicle_seat.rb
new file mode 100644
index 000000000..89871b8c6
--- /dev/null
+++ b/app/models/spree_cm_commissioner/vehicle_seat.rb
@@ -0,0 +1,11 @@
+module SpreeCmCommissioner
+ class VehicleSeat < SpreeCmCommissioner::Base
+ belongs_to :vehicle_type, class_name: 'SpreeCmCommissioner::VehicleType', dependent: :destroy
+ has_many :line_item_seats, class_name: 'SpreeCmCommissioner::LineItemSeat', dependent: :destroy
+ counter_culture :vehicle_type, column_name: proc { |model|
+ model.seat_type.in?(%w[normal vip]) && model.vehicle_type.allow_seat_selection == true ? 'vehicle_seats_count' : nil # rubocop:disable Layout/LineLength
+ },
+ column_names: { ['cm_vehicle_seats.seat_type IN (?)', %i[0 2]] => 'vehicle_seats_count' }
+ enum seat_type: %i[normal empty vip driver].freeze
+ end
+end
diff --git a/app/models/spree_cm_commissioner/vehicle_type.rb b/app/models/spree_cm_commissioner/vehicle_type.rb
new file mode 100644
index 000000000..43b089a2f
--- /dev/null
+++ b/app/models/spree_cm_commissioner/vehicle_type.rb
@@ -0,0 +1,76 @@
+module SpreeCmCommissioner
+ class VehicleType < SpreeCmCommissioner::Base
+ include SpreeCmCommissioner::RouteType
+
+ after_commit :recalculate_vehicle_seats_count, if: -> { saved_change_to_allow_seat_selection? && allow_seat_selection }
+ after_commit :set_vehicles_attributes
+
+ has_many :vehicle_seats, class_name: 'SpreeCmCommissioner::VehicleSeat', dependent: :destroy
+ has_many :option_value_vehicle_types, class_name: 'SpreeCmCommissioner::OptionValueVehicleType'
+ has_many :option_values, through: :option_value_vehicle_types, class_name: 'Spree::OptionValue'
+ has_many :vehicles, class_name: 'SpreeCmCommissioner::Vehicle', dependent: :destroy
+ belongs_to :vendor, class_name: 'Spree::Vendor'
+
+ validates :code, presence: true
+ validates :code, uniqueness: true
+ validates :name, presence: true
+ validates :name, uniqueness: true
+ accepts_nested_attributes_for :vehicle_seats, allow_destroy: true
+
+ state_machine :status, initial: :draft do
+ event :draft do
+ transition to: :draft
+ end
+ after_transition to: :draft, do: :after_draft
+
+ event :activate do
+ transition to: :active
+ end
+ after_transition to: :active, do: :after_activate
+
+ event :archive do
+ transition to: :archived
+ end
+ after_transition to: :archived, do: :after_archive
+ end
+
+ self.whitelisted_ransackable_attributes = %w[name code route_type id status]
+
+ def after_activate; end
+ def after_archive; end
+ def after_draft; end
+
+ def set_vehicles_attributes
+ vehicles.each(&:set_attributes)
+ vehicles.each(&:save)
+ end
+
+ def recalculate_vehicle_seats_count
+ self.vehicle_seats_count = vehicle_seats.where(seat_type: %w[normal vip]).count
+ save
+ end
+
+ def seat_layers
+ grouped_seats = SpreeCmCommissioner::VehicleSeat.where(vehicle_type_id: id).group_by(&:layer).transform_values do |seats|
+ seats.group_by(&:row).transform_values do |row_seats|
+ row_seats.sort_by(&:column).map do |seat|
+ {
+ row: seat.row,
+ column: seat.column,
+ label: seat.label,
+ layer: seat.layer,
+ seat_type: seat.seat_type,
+ created_at: seat.created_at,
+ vehicle_type_id: seat.vehicle_type_id
+ }
+ end
+ end
+ end
+ grouped_seats.map do |_layer, layer_seats|
+ layer_seats.map do |_row, row_seats|
+ row_seats
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/spree_cm_commissioner/vendor_decorator.rb b/app/models/spree_cm_commissioner/vendor_decorator.rb
index d9f8ce30e..49edf8e66 100644
--- a/app/models/spree_cm_commissioner/vendor_decorator.rb
+++ b/app/models/spree_cm_commissioner/vendor_decorator.rb
@@ -27,9 +27,18 @@ def self.prepended(base)
base.has_many :vendor_kind_option_values,
through: :option_value_vendors, source: :option_value
+ base.has_many :branches, class_name: 'SpreeCmCommissioner::Branch'
+ base.has_many :stops, class_name: 'SpreeCmCommissioner::Stop'
+
base.has_many :places,
through: :nearby_places, source: :place, class_name: 'SpreeCmCommissioner::Place'
+ base.has_many :vendor_stops, class_name: 'SpreeCmCommissioner::VendorStop', dependent: :destroy
+ base.has_many :boarding_points, -> { where(cm_vendor_stops: { stop_type: 0 }) },
+ through: :vendor_stops, source: :stop, class_name: 'Spree::Taxon'
+ base.has_many :drop_off_points, -> { where(cm_vendor_stops: { stop_type: 1 }) },
+ through: :vendor_stops, source: :stop, class_name: 'Spree::Taxon'
+
base.has_one :logo, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::VendorLogo'
base.has_one :payment_qrcode, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::VendorPaymentQrcode'
base.has_one :web_promotion_banner, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::VendorWebPromotionBanner'
@@ -55,6 +64,9 @@ def self.prepended(base)
base.has_many :subscriptions, through: :customers, class_name: 'SpreeCmCommissioner::Subscription'
base.has_many :subscription_orders, through: :subscriptions, class_name: 'Spree::Order', source: :orders
+ base.has_many :vehicle_types, class_name: 'SpreeCmCommissioner::VehicleType', dependent: :destroy
+ base.has_many :vehicles, through: :vehicle_types, class_name: 'SpreeCmCommissioner::Vehicle', dependent: :destroy
+
# TODO: we will need searchkick later
# unless Rails.env.test?
# base.searchkick(
diff --git a/app/models/spree_cm_commissioner/vendor_stop.rb b/app/models/spree_cm_commissioner/vendor_stop.rb
new file mode 100644
index 000000000..39be4113d
--- /dev/null
+++ b/app/models/spree_cm_commissioner/vendor_stop.rb
@@ -0,0 +1,9 @@
+require_dependency 'spree_cm_commissioner'
+module SpreeCmCommissioner
+ class VendorStop < ApplicationRecord
+ belongs_to :vendor, class_name: 'Spree::Vendor'
+ belongs_to :stop, class_name: 'Spree::Taxon'
+
+ enum stop_type: { boarding: 0, drop_off: 1 }
+ end
+end
diff --git a/app/overrides/spree/admin/option_types/_option_value_fields/option_value_replacer.html.erb.deface b/app/overrides/spree/admin/option_types/_option_value_fields/option_value_replacer.html.erb.deface
index 01fc10a03..7f84b3f1d 100644
--- a/app/overrides/spree/admin/option_types/_option_value_fields/option_value_replacer.html.erb.deface
+++ b/app/overrides/spree/admin/option_types/_option_value_fields/option_value_replacer.html.erb.deface
@@ -15,10 +15,14 @@
<%= f.number_field :presentation, step: 1, class: "form-control", required: true %>
<% when "string" %>
<%= f.text_field :presentation, class: "form-control", required: true %>
+ <% when "origin" %>
+ <%= f.text_field :presentation, class: "form-control", required: true %>
+ <% when "destination" %>
+ <%= f.text_field :presentation, class: "form-control", required: true %>
<% when "boolean" %>
- <%= f.radio_button :presentation, '1' %>
+ <%= f.radio_button :presentation, '1' %>
<%= f.label :presentation, 'true', :value => '1' %>
<%= f.radio_button :presentation, '0' %>
@@ -31,6 +35,10 @@
<%= f.text_field :presentation, class: "form-control", required: true %>
<% when "state_selection" %>
<%= f.text_field :presentation, class: "form-control", required: true %>
+ <% when "amenity" %>
+
<%= f.text_field :presentation, class: "form-control", required: true %>
+ <% when "vehicle" %>
+
<%= f.text_field :presentation, class: "form-control", required: true %>
<% end %>
diff --git a/app/overrides/spree/admin/option_types/index/option_row.html.erb.deface b/app/overrides/spree/admin/option_types/index/option_row.html.erb.deface
index 84f7d07bf..9e0b93e3b 100644
--- a/app/overrides/spree/admin/option_types/index/option_row.html.erb.deface
+++ b/app/overrides/spree/admin/option_types/index/option_row.html.erb.deface
@@ -11,6 +11,7 @@
<% @badge_class = 'badge-primary' if option_type.kind == "variant" %>
<% @badge_class = 'badge-secondary' if option_type.kind == "product" %>
<% @badge_class = 'badge-dark' if option_type.kind == "vendor" %>
+ <% @badge_class = 'badge-info' if option_type.kind == "vehicle_type" %>
<%= content_tag(:strong, class: "badge #{@badge_class} text-uppercase ") do; option_type.kind ; end %>
<%= active_badge(option_type.filterable) %>
diff --git a/app/queries/spree_cm_commissioner/trip_search_query.rb b/app/queries/spree_cm_commissioner/trip_search_query.rb
new file mode 100644
index 000000000..47b76a871
--- /dev/null
+++ b/app/queries/spree_cm_commissioner/trip_search_query.rb
@@ -0,0 +1,76 @@
+module SpreeCmCommissioner
+ class TripSearchQuery
+ attr_reader :origin_id, :destination_id, :vendor_id, :date
+
+ def initialize(origin_id:, destination_id:, date:, vendor_id: nil)
+ @origin_id = origin_id
+ @destination_id = destination_id
+ @vendor_id = vendor_id
+ @date = date
+ end
+
+ def call
+ trips_info.map do |trip|
+ trip_result_options = {
+ trip_id: trip.trip_id,
+ vendor_id: trip.vendor_id,
+ vendor_name: trip.vendor_name,
+ route_name: trip.route_name,
+ short_name: trip.short_name,
+ detail: trip.trip_id,
+ origin_id: trip.origin_id,
+ origin: trip.origin,
+ destination_id: trip.destination_id,
+ destination: trip.destination,
+ total_sold: trip.total_sold,
+ total_seats: trip.total_seats,
+ vehicle_id: trip.vehicle_id,
+ departure_time: trip.departure_time.strftime('%H:%M'),
+ duration: trip.duration
+ }
+ SpreeCmCommissioner::TripResult.new(trip_result_options)
+ end
+ end
+
+ def trips_info
+ result = Spree::Variant.select('spree_variants.id as trip_id,
+ spree_vendors.id as vendor_id,
+ spree_vendors.name as vendor_name,
+ routes.name as route_name,
+ routes.short_name as short_name,
+ details.id as detail_id,
+ boarding.stop_id as origin_id,
+ drop_off.stop_id as destination_id,
+ details.departure_time as departure_time,
+ details.duration as duration,
+ boarding.stop_name as origin,
+ drop_off.stop_name as destination,
+ COALESCE(ts.total_sold, 0) as total_sold,
+ cm_vehicles.number_of_seats as total_seats,
+ cm_vehicles.id as vehicle_id
+ '
+ )
+ .joins('INNER JOIN spree_products routes ON routes.id = spree_variants.product_id')
+ .joins('INNER JOIN cm_trips details on details.product_id = routes.id')
+ .joins('INNER JOIN cm_vehicles ON cm_vehicles.id = details.vehicle_id')
+ .joins("INNER JOIN cm_trip_stops boarding ON boarding.trip_id = details.id AND boarding.stop_type = '0'")
+ .joins("INNER JOIN cm_trip_stops drop_off ON drop_off.trip_id = details.id AND drop_off.stop_type = '1'")
+ .joins('INNER JOIN spree_vendors ON spree_vendors.id = spree_variants.vendor_id')
+ .joins("LEFT JOIN (#{total_sold.to_sql}) ts ON spree_variants.id = ts.trip_id")
+
+ # TODO: migrat to new table: vehicle, orign and destination
+ result = result.where(boarding: { stop_id: origin_id }, drop_off: { stop_id: destination_id })
+ result = result.where(vendor_id: vendor_id) if vendor_id.present?
+ result
+ end
+
+ def total_sold
+ Spree::Variant.select('spree_variants.id as trip_id, SUM(spree_line_items.quantity) as total_sold')
+ .joins('INNER JOIN spree_line_items ON spree_line_items.variant_id = spree_variants.id')
+ .joins('INNER JOIN spree_orders ON spree_orders.id = spree_line_items.order_id')
+ .where(spree_orders: { state: 'complete' })
+ .where(spree_line_items: { date: date })
+ .group('spree_variants.id')
+ end
+ end
+end
diff --git a/app/queries/spree_cm_commissioner/trip_seat_layout_query.rb b/app/queries/spree_cm_commissioner/trip_seat_layout_query.rb
new file mode 100644
index 000000000..9072cbc14
--- /dev/null
+++ b/app/queries/spree_cm_commissioner/trip_seat_layout_query.rb
@@ -0,0 +1,76 @@
+module SpreeCmCommissioner
+ class TripSeatLayoutQuery
+ attr_reader :trip_id, :date, :vehicle_type, :vehicle, :route
+
+ def initialize(trip_id:, date:)
+ @trip_id = trip_id
+ @date = date
+ @route = Spree::Variant.find_by(id: trip_id).product
+ @vehicle = SpreeCmCommissioner::Vehicle.find_by(id: route.trip.vehicle_id)
+ @vehicle_type = SpreeCmCommissioner::VehicleType.find_by(id: vehicle.vehicle_type_id)
+ end
+
+ def call
+ allow_seat_selection = @route.trip.allow_seat_selection
+ total_sold, remaining_seats, layout = process_seat_selection(allow_seat_selection)
+
+ SpreeCmCommissioner::TripSeatLayoutResult.new({ trip_id: trip_id, total_sold: total_sold,
+ total_seats: vehicle.number_of_seats,
+ remaining_seats: remaining_seats,
+ layout: layout,
+ allow_seat_selection: allow_seat_selection
+ }
+ )
+ end
+
+ def process_seat_selection(allow_seat_selection)
+ # layout_structure
+ # {"First_Layer" => {"Row_1" => [{seat1}, {seat2}, {seat3}, {seat4}],
+ # {"Row_2" => [{seat5}, {seat6}, {seat7}, {seat8}],
+ # }
+ if allow_seat_selection
+ vehicle_seats = seats.to_a
+ total_sold = vehicle_seats.count { |s| s.seat_id.present? }
+ remaining_seats = vehicle.number_of_seats - total_sold
+ layout = vehicle_seats.group_by(&:layer).transform_values do |s|
+ s.group_by(&:row).transform_values do |r|
+ r.sort_by(&:column).map do |seat|
+ {
+ row: seat.row,
+ column: seat.column,
+ label: seat.label,
+ layer: seat.layer,
+ seat_type: seat.seat_type,
+ created_at: seat.created_at,
+ seat_id: seat.seat_id,
+ vehicle_type_id: seat.vehicle_type_id
+ }
+ end
+ end
+ end
+ else
+ total_sold = Spree::LineItem.joins(:order)
+ .where(variant_id: trip_id, date: date, spree_orders: { state: 'complete' })
+ .sum(:quantity)
+ remaining_seats = vehicle.number_of_seats - total_sold
+ layout = nil
+ end
+
+ [total_sold, remaining_seats, layout]
+ end
+
+ def seats
+ SpreeCmCommissioner::VehicleSeat.select('cm_vehicle_seats.*, os.seat_id as seat_id')
+ .joins("LEFT JOIN (#{ordered_seats.to_sql}) os ON cm_vehicle_seats.id = os.seat_id")
+ .where('cm_vehicle_seats.vehicle_type_id = ? ', vehicle_type.id)
+ end
+
+ def ordered_seats
+ SpreeCmCommissioner::LineItemSeat.select('cm_line_item_seats.seat_id')
+ .joins('INNER JOIN spree_line_items ON cm_line_item_seats.line_item_id = spree_line_items.id')
+ .joins('INNER JOIN spree_orders ON spree_orders.id = spree_line_items.order_id')
+ .where('spree_orders.state = ? ', 'complete')
+ .where('cm_line_item_seats.variant_id = ? AND cm_line_item_seats.date = ?', trip_id, date)
+ end
+ end
+end
diff --git a/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb b/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb
new file mode 100644
index 000000000..31eeee8e9
--- /dev/null
+++ b/app/services/spree_cm_commissioner/vehicle_option_value_creator.rb
@@ -0,0 +1,11 @@
+module SpreeCmCommissioner
+ class VehicleOptionValueCreator
+ def self.call(vehicle)
+ vehicle_option_type = Spree::OptionType.vehicle
+
+ value = vehicle_option_type.option_values.where(name: vehicle.id).first_or_initialize
+ value.presentation = vehicle.code
+ value.save
+ end
+ end
+end
diff --git a/app/views/spree/admin/vendor_service_calendars/_form.html.erb b/app/views/spree/admin/vendor_service_calendars/_form.html.erb
index 59d6ae14d..4993faf19 100644
--- a/app/views/spree/admin/vendor_service_calendars/_form.html.erb
+++ b/app/views/spree/admin/vendor_service_calendars/_form.html.erb
@@ -1,4 +1,4 @@
-