diff --git a/Gemfile b/Gemfile index 9707b3800..552ca5176 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'rails', '~> 4.1.9' gem 'mysql2', '~> 0.3.18' gem 'rake', '~> 10.4.2' gem 'rdoc', '~> 4.2.0' +gem 'jbuilder', '~> 2.4.0' # simulate environment variables gem 'dotenv-rails', '~> 2.0.2', :require => 'dotenv/rails-now' @@ -35,6 +36,8 @@ gem 'nilify_blanks', '~> 1.2.0' # ui gem 'jquery-rails', '~> 3.1.2' gem 'jquery-ui-rails', '~> 5.0.3' +gem 'fullcalendar-rails', '~> 2.4.0.0' +gem 'momentjs-rails', '~> 2.10.6' gem 'rails4-autocomplete', '~> 1.1.1' gem 'select2-rails', '~> 3.5.9.3' gem 'kaminari', '~> 0.16.3' diff --git a/Gemfile.lock b/Gemfile.lock index 5436955ae..979356d66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -133,6 +133,9 @@ GEM font-awesome-rails (4.4.0.0) railties (>= 3.2, < 5.0) formatador (0.2.5) + fullcalendar-rails (2.4.0.0) + jquery-rails (>= 3.1.1, < 5.0.0) + momentjs-rails (>= 2.9.0) fuubar (2.0.0) rspec (~> 3.0) ruby-progressbar (~> 1.4) @@ -165,6 +168,9 @@ GEM activesupport (>= 4.1.9) loofah (>= 2.0) nokogiri (~> 1.6) + jbuilder (2.4.1) + activesupport (>= 3.0.0, < 5.1) + multi_json (~> 1.2) jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) @@ -197,6 +203,8 @@ GEM mime-types (2.4.3) mini_portile (0.6.2) minitest (5.8.2) + momentjs-rails (2.10.6) + railties (>= 3.1) multi_json (1.10.1) multipart-post (2.0.0) mysql2 (0.3.18) @@ -428,16 +436,19 @@ DEPENDENCIES factory_girl_rails (~> 4.5.0) ffaker (~> 1.32.1) font-awesome-rails (~> 4.4.0) + fullcalendar-rails (~> 2.4.0.0) fuubar (~> 2.0.0) guard-livereload (~> 2.4.0) guard-rspec (~> 4.5.0) highline (~> 1.7.2) inline_svg + jbuilder (~> 2.4.0) jquery-rails (~> 3.1.2) jquery-ui-rails (~> 5.0.3) kaminari (~> 0.16.3) letter_opener (~> 1.3.0) letter_opener_web (~> 1.3.0) + momentjs-rails (~> 2.10.6) mysql2 (~> 0.3.18) net-ldap (~> 0.11) nilify_blanks (~> 1.2.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index bbede28e1..7a8522469 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -20,6 +20,8 @@ //= require select2 //= require jquery.sticky.js //= require jquery.dotdotdot.js +//= require moment +//= require fullcalendar //= require_tree //= require_self @@ -98,6 +100,63 @@ $(document).ready(function() { $("#res-history-future").DataTable().order([2, "asc"]).draw(); $("#res-history-overdue,#res-history-past,#res-history-past_overdue").DataTable().order([4, "desc"]).draw(); + // For availability calendars + $('#avail-cal').fullCalendar({ + // generate event array locally for speed + events: function(start, end, timezone, callback) { + var events = []; + var data = JSON.parse($('#avail-cal').attr('data-src')); + + for(var i=0; i= start) { + if(date > end) { + break; + } else { + var event = { start: date, end: date, title: json_event.title, allDay: true, backgroundColor: json_event.color, borderColor: json_event.color }; + events.push(event); + } + } + } + + callback(events); + }, + viewRender: function(currentView) { + // prevent from scrolling beyond boundaries + var minDate = moment($('#avail-cal').attr('data-min')), + maxDate = moment($('#avail-cal').attr('data-max')); + + // Past + if (minDate >= currentView.start && minDate <= currentView.end) { + $(".fc-prev-button").prop('disabled', true); + $(".fc-prev-button").addClass('fc-state-disabled'); + } + else { + $(".fc-prev-button").removeClass('fc-state-disabled'); + $(".fc-prev-button").prop('disabled', false); + } + + // Future + if (maxDate >= currentView.start && maxDate <= currentView.end) { + $(".fc-next-button").prop('disabled', true); + $(".fc-next-button").addClass('fc-state-disabled'); + } else { + $(".fc-next-button").removeClass('fc-state-disabled'); + $(".fc-next-button").prop('disabled', false); + } + }, + eventRender: function(event, element) { + element.attr('data-role', 'avail-item'); + element.css({ 'text-align': 'center', 'font-size': '2em' }); + }, + defaultView: 'basicWeek', + header: { left: 'prev,next', + center: '', + right: '' }, + aspectRatio: 4.4 + }); + // ### REPORTS JS ### // diff --git a/app/assets/javascripts/calendar.js b/app/assets/javascripts/calendar.js deleted file mode 100644 index 981f081dc..000000000 --- a/app/assets/javascripts/calendar.js +++ /dev/null @@ -1,130 +0,0 @@ -function decCellValue(cell) { - var obj = cell.children('.num').children()[0]; - obj.innerHTML = Math.max(parseInt(obj.innerHTML) - 1, 0); -}; - -function parseDate(dateString){ -//why the fck cant we have normal datestrings - var d = new Date(dateString); - var string = d.toISOString(); - return string.substring(5,7) + "/" + string.substring(8,10) + '/' + string.substring(0,4); -} - -function dateInTimeZone(dateString) { - // parse an ISO date string and returns - // midnight of that date in the system time - date = new Date(dateString); - date.setTime(date.getTime() + date.getTimezoneOffset()*60*1000); - return date; -} - -function dateToRubyString(date) { - return date.toISOString().substring(0,10); -}; - -function dateToHeading(date) { - var days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat','Sun']; - return ' ' + days[date.getUTCDay()]; -}; - -function renderCalendar(reservations, week_start, max, blackouts) { - //set initial values and date ids - var date = new Date(week_start.getTime()); - $('.calendar_cell').each(function() { - $(this).children('.head').children()[0].innerHTML = date.getUTCDate().toString(); - $(this).children('.head').children()[1].innerHTML = dateToHeading(date); - $(this).children('.num').children()[0].innerHTML = max.toString(); - $(this).attr('id',dateToRubyString(date)); - date.setDate(date.getDate()+1); - }); - var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; - - $('.month').children()[0].innerHTML = months[week_start.getMonth()] + " " + date.getFullYear().toString(); - - //set cell values based on reservations - var week_end = new Date(week_start.getTime()); - week_end.setDate(week_start.getDate() + 6); - - for(var d = 0; d < reservations.length; d++) { - var end = dateInTimeZone(reservations[d].end); - var start = dateInTimeZone(reservations[d].start); - if ((start < week_end) && (end >= week_start)) { - //for each reservation, decrement availability per day - var begin_date = ((week_start > start) ? week_start : start); - var end_date = ((week_end < end) ? week_end : end); - for (var date = new Date(begin_date.getTime()); - date <= end_date; - date.setDate(date.getDate()+1)) { - decCellValue($('#'+dateToRubyString(date))); - } - } - } - - //color cells appropriately - $('.calendar_cell').each(function() { - var blacked = false; - for(var b = 0; b < blackouts.length; b++) { - date = dateInTimeZone($(this).attr('id')); - if ((new Date(blackouts[b].start) <= date) && (new Date(blackouts[b].end) >= date)) { - blacked = true; - break; - } - } - if (blacked) { - var color = '#aaaaaa'; - } else { - var val = parseInt($(this).children('.num').children()[0].innerHTML); - if (val == 0) { - var color = 'rgba(255,0,0,0.3)' - } else { - var red = Math.min(Math.floor(510 - val*510/max),255).toString(); - var green = Math.min(Math.floor(val*510/max),255).toString(); - var color = 'rgba(' + red + ',' + green + ',0,0.3)'; - } - } - $(this).css("background-color",color); - - }); - -}; - -function shiftCalendar(offset) { - var reservations = $('#res-data').data('url'); - var blackouts = $('#res-data').data('blackouts'); - var week_start = dateInTimeZone($('.calendar_cell').first().attr('id')); - var today = dateInTimeZone($('#res-data').data('today')); - var date_max = dateInTimeZone($('#res-data').data('dateMax')); - var max = $('#res-data').data('max'); - - week_start.setDate(week_start.getDate() + offset); - block_left = week_start <= today; - if (block_left) { - week_start.setTime(today.getTime()); - } - $('.c-left').children().toggleClass('disabled-control',block_left); - - block_right = week_start >= date_max; - if (block_right) { - week_start.setTime(date_max.getTime()); - } - $('.c-right').children().toggleClass('disabled-control',block_right); - renderCalendar(reservations,week_start,max,blackouts); -}; - - -$('#reservation-calendar').ready(function() { - - //quit if no reservation calendar present - //there's probably a better way to do this? - - if ($('#reservation-calendar').size() == 0) { - return false; - } - - shiftCalendar(0); - - $('.control').click(function() { - shiftCalendar(parseInt($(this).attr('change'))); - }); - -}); diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 718973eca..3d89f35e0 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -27,6 +27,9 @@ @import "jquery-ui-1.10.3.theme"; @import "jquery_overrides"; +// Calendar assets +@import "fullcalendar"; + // Autocomplete CSS // ============================ @import "autocomplete"; diff --git a/app/assets/stylesheets/equipment_models/_show.css.scss b/app/assets/stylesheets/equipment_models/_show.css.scss index c58db7c91..812cfd832 100644 --- a/app/assets/stylesheets/equipment_models/_show.css.scss +++ b/app/assets/stylesheets/equipment_models/_show.css.scss @@ -4,76 +4,13 @@ $border-att: 1px solid darken($body-bg, 20%); text-align:center; } -.reservation_calendar { - overflow: hidden; -} - -#calendar-ul { - margin-left: 0; - margin-bottom: 3px; - margin-top: 3px; - padding-left: 0; -} - -.calendar_cell { - border: $border-att; - border-radius: 3px; - width: 13%; - display: inline-block; - vertical-align:top; - background-color: rgba(0,255,0,0.5); - // opacity: 0.9; - padding-top: 3px; - padding-bottom: 5px; - box-sizing: border-box; - } - -.month { - margin-top:3px; -} - -.calendar_cell:hover{ - // opacity: 1.0; - // cursor: pointer; - cursor: default; -} - -.c-left { - float: left; -} - -.c-right { - float: right; -} - -.control { - cursor:pointer; - margin-left:3px; - margin-right: 3px; - opacity: 0.8; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.control:not(.disabled-control):hover{ - opacity: 1.0; -} - -.control:active{ - color: black; -} - -.lite { - opacity: 0.7; +.fc-day-header { + border-style: none !important; + padding: 0 1.2em !important; } -.disabled-control { - opacity:0.2; - cursor:default; +.fc-widget-header, .fc-widget-content, .fc-event-container, .fc-content-skeleton { + border-style: none !important; } .btn-large { diff --git a/app/controllers/equipment_models_controller.rb b/app/controllers/equipment_models_controller.rb index d924d2ab2..c53f87fea 100644 --- a/app/controllers/equipment_models_controller.rb +++ b/app/controllers/equipment_models_controller.rb @@ -6,7 +6,8 @@ class EquipmentModelsController < ApplicationController skip_before_action :authenticate_user!, only: [:show, :index], unless: :guests_disabled? before_action :set_equipment_model, - only: [:show, :edit, :update, :destroy, :deactivate] + only: [:show, :edit, :update, :destroy, :deactivate, + :availability] before_action :set_category_if_possible, only: [:index, :new] include ActivationHelper @@ -31,6 +32,7 @@ def index end def show # rubocop:disable AbcSize, MethodLength + calculate_availability relevant_reservations = Reservation.active.for_eq_model(@equipment_model) @associated_equipment_models = @equipment_model.associated_equipment_models.sample(6) @@ -184,4 +186,47 @@ def fix_content_type(filedata) def type_from_file_command(file) Paperclip::FileCommandContentTypeDetector.new(file).detect end + + def calculate_availability # rubocop:disable all + # get start and end dates + @start_date = Date.today.beginning_of_week(:sunday) + @end_date = (Date.today + 1.month).end_of_week(:sunday) + + # hack-y, we have a proper scope in master for this + reservations = + Reservation.for_eq_model(@equipment_model).finalized + .where('start_date <= ? and due_date >= ?', @end_date, @start_date) + \ + Reservation.for_eq_model(@equipment_model).overdue + max_avail = @equipment_model.equipment_items.active.count + + @avail_data = [] + (@start_date..@end_date).map do |date| + count = 0 + # also hack-y, we need to fix these methods + reservations.each do |res| + if res.overdue && res.checked_out + count += 1 + elsif res.start_date <= date && res.due_date >= date + count += 1 + end + end + + availability = max_avail - count + + # set up colors + if date < Time.zone.today + color = '#888' + elsif availability == 0 + color = '#d9534f' + elsif availability == max_avail + color = '#5cb85c' + elsif availability > [0.25 * max_avail, 1].max + color = '#94d194' + elsif availability <= [0.25 * max_avail, 1].max + color = '#f0ad4e' + end + @avail_data << { title: availability.to_s, start: date, end: date, + allDay: true, color: color } + end + end end diff --git a/app/views/equipment_models/_calendar.html.erb b/app/views/equipment_models/_calendar.html.erb index 36c47bbe4..5d622a764 100644 --- a/app/views/equipment_models/_calendar.html.erb +++ b/app/views/equipment_models/_calendar.html.erb @@ -1,29 +1,3 @@

Availability

-
- <%= content_tag "div", id: "res-data", data:{url:@reservation_data, blackouts:@blackouts, today:@date.to_s, max:@max, date_max: @date_max.to_s} do %> - <% end %> -
- - - -
- - -
-
- - -
-
+
\ No newline at end of file diff --git a/app/views/equipment_models/show.html.erb b/app/views/equipment_models/show.html.erb index 50feea4c3..fb412d696 100644 --- a/app/views/equipment_models/show.html.erb +++ b/app/views/equipment_models/show.html.erb @@ -29,10 +29,10 @@
-
+
<%= render :partial => 'calendar' %>
-
+
<%= render :partial => 'add_to_cart' %>