Skip to content

Commit

Permalink
Merge pull request #1654 from uOttawa-Makerspace/feat/locker-management
Browse files Browse the repository at this point in the history
Locker Management
  • Loading branch information
PencilAmazing authored Jan 6, 2025
2 parents e23cca8 + 9b0d3f3 commit e606975
Show file tree
Hide file tree
Showing 41 changed files with 1,133 additions and 3 deletions.
20 changes: 20 additions & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,26 @@ u {
color: $primary;
}

// Buttons created with button_to are actually a form tag with a single button inside
// Those don't act normally inside button groups, so try to clean those up a bit
.btn-group {
// Select all form buttons inside groups
form.button_to {
// if form butotn has anything to right
&:has(+ *) > .btn {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
// if form button has anything to left
& + * > .btn {
border-lelft: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}

// These are used everywhere, only reason they're kept as legacy
.x-button {
color: #af0000;
Expand Down
146 changes: 146 additions & 0 deletions app/controllers/locker_rentals_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

class LockerRentalsController < ApplicationController
before_action :current_user
before_action :signed_in, except: %i[stripe_success stripe_cancelled]
# Also sets @locker_rental
before_action :check_permission,
except: %i[index new stripe_success stripe_cancelled]

def index
# Only admin can see index list
#redirect_to :new_locker_rental unless current_user.admin?

@own_locker_rentals = current_user.locker_rentals
@locker_types = LockerType.all
@locker_rentals =
LockerRental.includes(:locker_type, :rented_by).order(
locker_type_id: :asc
)
end

def show
@stripe_checkout_session =
Stripe::Checkout::Session.create(
success_url: stripe_success_locker_rentals_url,
cancel_url: stripe_cancelled_locker_rentals_url,
mode: "payment",
line_items: @locker_rental.locker_type.generate_line_items,
billing_address_collection: "required",
client_reference_id: "locker-rental-#{@locker_rental.id}"
)
end

def new
@locker_rental = LockerRental.new
# Only locker types enabled by admin
new_instance_attributes
end

def create
@locker_rental = LockerRental.new(locker_rental_params)
# if not admin member or has debug value set
# then force to wait for admin approval
if !current_user.admin? || params.dig(:locker_rental, :ask)
@locker_rental.state = :reviewing
@locker_rental.rented_by = current_user
end

if @locker_rental.save
redirect_back fallback_location: :new_locker_rental
else
new_instance_attributes
render :new, status: :unprocessable_entity
end
end

def update
@locker_rental = LockerRental.find(params[:id])

# NOTE move this line to model maybe?
# if changing state to 'active'
if locker_rental_params[:state] == "active"
# default to end of semester
@locker_rental.owned_until ||= end_of_this_semester
# Get first available locker specifier if not set
@locker_rental.locker_specifier ||=
@locker_rental.locker_type.get_available_lockers.first
end

if @locker_rental.update(locker_rental_params)
flash[:notice] = "Locker rental updated"
else
flash[:alert] = "Failed to update locker rental" + helpers.tag.br +
@locker_rental.errors.full_messages.join(helpers.tag.br)
end

redirect_back fallback_location: :locker_rentals
end

def stripe_success
end

def stripe_cancelled
end

private

def check_permission
# If user gives a request id
if params[:id].present?
@locker_rental = LockerRental.find(params[:id])
# Allow if getting own locker rental
return if @locker_rental.rented_by == current_user
end
# Always allow admin
return if current_user.admin?

# Else redirect to only page with no auth (new page takes no ID)
redirect_to :new_locker_rental
end

def new_instance_attributes
@available_locker_types = LockerType.available
# Who users can request as
# because we want to localize later
# FIXME this is not used for anything, pretty useless
@available_fors = {
staff: ("CEED staff member" if current_user.staff?),
student: ("GNG student" if current_user.student?),
general: "community member"
}.compact.invert
# Don't allow new request if user already has an active or pending request
@pending_locker_request = current_user.locker_rentals.under_review.first
end

def locker_rental_params
if current_user.admin? && !params.dig(:locker_rental, :ask)
admin_params =
params.require(:locker_rental).permit(
:locker_type_id,
# admin can assign and approve requests
:rented_by_id,
:locker_specifier,
:state,
:owned_until,
:notes
)
# FIXME replace that search with a different one, return ID instead
# If username is given (since search can do that)
rented_by_user =
User.find_by(username: params.dig(:locker_rental, :rented_by_username))
if rented_by_user
# then convert to user id
admin_params.reverse_merge!(rented_by_id: rented_by_user.id)
end
return admin_params
else
# people pick where they want a locker
# prevent user from editing rental notes after first submit
params
.require(:locker_rental)
.permit(:locker_type_id)
.tap { |p| p.permit(:notes) unless params[:id] }
end
end
end
55 changes: 55 additions & 0 deletions app/controllers/locker_types_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class LockerTypesController < AdminAreaController
def new
@locker_type = LockerType.new
end

def create
# the params might come with an :amount field, use that
# to create locker instances
@locker_type = LockerType.new(locker_type_params)
if @locker_type.save
flash[:notice] = "Locker type #{@locker_type.short_form} created"
redirect_to lockers_path
else
render :new, status: :unprocessable_entity
end
end

def edit
@locker_type = LockerType.find(params[:id])
end

def update
@locker_type = LockerType.find(params[:id])
if @locker_type.update(locker_type_params)
redirect_to lockers_path, notice: "Locker updated"
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@locker_type = LockerType.find(params[:id])
if @locker_type.destroy
flash[:notice] = "Locker type removed"
else
flash[
:alert
] = "Failed to delete locker type, records probably exist in history"
end
redirect_to lockers_path
end

private

def locker_type_params
params.require(:locker_type).permit(
:short_form,
:description,
:available,
:available_for,
:quantity,
:cost
)
end
end
36 changes: 36 additions & 0 deletions app/controllers/lockers_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class LockersController < AdminAreaController
before_action :current_user
before_action :signed_in

before_action do
unless current_user.admin?
flash[:alert] = "You cannot access this area"
redirect_back(fallback_location: root_path)
end
end

helper_method :rental_state_icon

def index
@locker_types = LockerType.all
@locker_requests_pending = LockerRental.under_review.take 5

# For the locker type form
@locker_type = LockerType.new
# For the locker rental form
@locker_rental = LockerRental.new
end

private

def rental_state_icon(state)
case state
when "active"
"fa-lock"
when "cancelled"
"fa-clock-o text-danger"
else
""
end
end
end
13 changes: 13 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ def load_rakes
require "rake"
MakerSpaceRepo::Application.load_tasks if Rake::Task.tasks.empty?
end

def end_of_this_semester
if DateTime.now.month in 9..12
# End of Fall
DateTime.new(DateTime.now.year, 12).end_of_month
elsif DateTime.now.month in 1..4
# End of Winter
DateTime.new(DateTime.now.year, 4).end_of_month
elsif DateTime.now.month in 5..8
# End of Summer
DateTime.new(DateTime.now.year, 8).end_of_month
end
end
end

def rgba(color, opacity)
Expand Down
67 changes: 67 additions & 0 deletions app/helpers/locker_rentals_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module LockerRentalsHelper
def locker_rental_actions(rental, for_admin = true)
end_rental_button =
button_to(
t("lockers.actions.cancel_rental"),
locker_rental_path(rental),
data: {
confirm: "Are you sure you want to cancel this locker rental?"
},
params: {
locker_rental: {
state: :cancelled
}
},
method: :put,
class: "btn btn-danger"
)
ask_payment_button =
button_to(
"Send to checkout",
locker_rental_path(rental),
data: {
confirm: "Are you sure you want to send request to checkout?"
},
params: {
locker_rental: {
state: :await_payment
}
},
method: :put,
class: "btn btn-success"
)
instant_approve_button =
button_to(
"Assign now",
locker_rental_path(rental),
data: {
confirm: "Are you sure you want to assign locker immediately?"
},
params: {
locker_rental: {
state: :active
}
},
method: :put,
class: "btn btn-info"
)

return end_rental_button unless for_admin

case rental.state.to_sym
when :reviewing
tag.div class: "btn-group" do
ask_payment_button + instant_approve_button + end_rental_button
end
when :await_payment
tag.div class: "btn-group" do
instant_approve_button + end_rental_button
end
when :active
tag.div class: "btn-group" do
end_rental_button
end
#when :cancelled
end
end
end
43 changes: 43 additions & 0 deletions app/javascript/entrypoints/locker_rental_form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import TomSelect from "tom-select";

const locker_specifier = new TomSelect("#locker_rental_locker_specifier", {
searchPlaceholder: "Select locker...",
// Hide options if not selected by locker_type
render: {
optgroup: function (data) {
let optgroup = document.createElement("div");
optgroup.className = "optgroup";
optgroup.appendChild(data.options);
return data.group.disabled == true ? null : optgroup;
},
},
});

// Add the disabled and hidden attribute to all optgroups
// except for selected type
function disableAllExcept(shortForm) {
locker_specifier.input.querySelectorAll("optgroup").forEach((optgroup) => {
optgroup.disabled = !(optgroup.label == shortForm);
optgroup.hidden = !(optgroup.label == shortForm);
});

// Some options are now removed, reselect new value
try {
let newVal = locker_specifier.input.querySelector(
`optgroup[label="${shortForm}"]`
).children[0].value;
locker_specifier.setValue(newVal);
} catch (e) {}
// re-render
locker_specifier.sync();
}

const locker_type = new TomSelect("#locker_rental_locker_type_id", {
onChange: (value) => {
disableAllExcept(locker_type.options[value].text);
},
});

locker_type.trigger("change", locker_type.getValue());
// window.locker_type = locker_type
window.locker_specifier = locker_specifier;
Loading

0 comments on commit e606975

Please sign in to comment.