mirror of
https://github.com/rickbarrette/redmine_qbo_calendar.git
synced 2026-10-03 20:40:43 -04:00
Add holiday management functionality with CRUD operations
- Implement HolidaysController for managing holidays - Create Holiday model with validation for recurrence types - Add holiday form and views for creating, editing, and listing holidays - Integrate holiday checks into customer appointment scheduling - Update locale file for holiday-related labels and error messages - Add migration for holidays table - Update plugin version and add holidays to admin menu
This commit is contained in:
1 parent
9a8787c3da
commit
167e956457
12 files changed
+407
-9
No files matched your search
@@ -12,6 +12,8 @@
|
||||
# Get Redmine's configured non-working week days (e.g., ["6", "7"] for Sat/Sun)
|
||||
non_working_days = Setting.non_working_week_days || []
|
||||
|
||||
all_holidays = Holiday.all
|
||||
|
||||
# Safely collect all unique CustomerAppointments across the entire calendar date range
|
||||
all_appointments = (calendar.startdt..calendar.enddt).flat_map { |date|
|
||||
calendar.events_on(date)
|
||||
@@ -26,15 +28,23 @@
|
||||
target_date = current_day.to_date
|
||||
|
||||
return false if target_date < start_date
|
||||
# Skip rendering on Redmine non-working days
|
||||
|
||||
# Skip rendering on Redmine standard non-working days
|
||||
return false if non_working_days.include?(target_date.cwday.to_s)
|
||||
|
||||
# Skip rendering on custom non-working days (Holidays)
|
||||
return false if all_holidays.any? { |h| h.occurs_on?(target_date) }
|
||||
|
||||
duration = [appointment.duration.to_i, 1].max
|
||||
date_cursor = start_date
|
||||
working_days_count = 0
|
||||
|
||||
while date_cursor <= target_date
|
||||
unless non_working_days.include?(date_cursor.cwday.to_s)
|
||||
is_standard_weekend = non_working_days.include?(date_cursor.cwday.to_s)
|
||||
is_custom_holiday = all_holidays.any? { |h| h.occurs_on?(date_cursor) }
|
||||
|
||||
# Only increment duration count if it's a standard working day AND not a holiday
|
||||
unless is_standard_weekend || is_custom_holiday
|
||||
working_days_count += 1
|
||||
end
|
||||
|
||||
@@ -135,6 +145,11 @@
|
||||
|
||||
<%
|
||||
is_today = (day == User.current.today)
|
||||
|
||||
# Find the specific holiday for this day (if any)
|
||||
current_holiday = all_holidays.find { |h| h.occurs_on?(day) }
|
||||
is_custom_holiday = current_holiday.present?
|
||||
is_standard_weekend = non_working_days.include?(day.cwday.to_s)
|
||||
|
||||
# Filter out standard events if "only appointments" toggle is active
|
||||
standard_events = only_appointments ? [] : calendar.events_on(day).reject { |e| e.is_a?(CustomerAppointment) }
|
||||
@@ -169,7 +184,8 @@
|
||||
end
|
||||
%>
|
||||
|
||||
<li class="<%= calendar.day_css_classes(day) %> calbody"<%= ' id="today"'.html_safe if is_today %>>
|
||||
<%# Added 'week-end' class and background color styling for custom holidays to mimic weekends %>
|
||||
<li class="<%= calendar.day_css_classes(day) %> calbody <%= 'week-end' if is_custom_holiday %>" style="<%= 'background-color: #f6f6f6;' if is_custom_holiday %>"<%= ' id="today"'.html_safe if is_today %>>
|
||||
|
||||
<p class="day-num" style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 4px;">
|
||||
<!-- Left: Day number and abbreviated weekday -->
|
||||
@@ -182,7 +198,8 @@
|
||||
|
||||
<!-- Center: New Appointment Action -->
|
||||
<span class="day-num-center" style="display: inline-flex; align-items: center; justify-self: center;">
|
||||
<% if day >= User.current.today && !non_working_days.include?(day.cwday.to_s) %>
|
||||
<%# Only show ADD button if it's in the future, NOT a weekend, and NOT a custom holiday %>
|
||||
<% if day >= User.current.today && !is_standard_weekend && !is_custom_holiday %>
|
||||
<%= link_to sprite_icon('add'),
|
||||
new_customer_appointment_path(start_date: day),
|
||||
class: 'icon-only icon-add',
|
||||
@@ -200,6 +217,17 @@
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<%# Render Holiday Name Badge if it's a holiday %>
|
||||
<% if is_custom_holiday %>
|
||||
<div class="holiday-badge" style="background-color: #f2dede; color: #a94442; padding: 3px 6px; border-radius: 3px; font-size: 0.8em; font-weight: bold; margin-bottom: 4px; border: 1px solid #ebccd1; display: flex; align-items: center; gap: 4px;">
|
||||
<span class="fa-stack fa-lg">
|
||||
<i class="fas fa-calendar fa-stack-1x"></i>
|
||||
<i class="fas fa-slash fa-stack-2x" style="color:Tomato"></i>
|
||||
</span>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><%= current_holiday.name %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% day_events.each do |event| %>
|
||||
|
||||
<% if event.is_a?(Issue) %>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<%= error_messages_for 'holiday' %>
|
||||
|
||||
<div class="box tabular">
|
||||
<p>
|
||||
<label for="holiday_name"><%= l(:field_name, default: 'Name') %> <span class="required">*</span></label>
|
||||
<%= f.text_field :name, required: true, size: 40 %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="holiday_is_recurring"><%= l(:field_is_recurring, default: 'Is this a recurring holiday?') %></label>
|
||||
<%= f.check_box :is_recurring, id: 'holiday_is_recurring' %>
|
||||
</p>
|
||||
|
||||
<p id="p_recurrence_type" style="display: none;">
|
||||
<label for="holiday_recurrence_type"><%= l(:field_recurrence_type, default: 'Recurrence Type') %></label>
|
||||
<%= f.select :recurrence_type,
|
||||
Holiday::TYPES.map { |type_key|
|
||||
label_text = case type_key
|
||||
when 'fixed' then l(:label_recurrence_fixed, default: 'Fixed Date (e.g., Dec 25)')
|
||||
when 'dynamic' then l(:label_recurrence_dynamic, default: 'Dynamic Date (e.g., 1st Monday of Sept)')
|
||||
else type_key.humanize
|
||||
end
|
||||
[label_text, type_key]
|
||||
},
|
||||
{},
|
||||
id: 'holiday_recurrence_type' %>
|
||||
</p>
|
||||
|
||||
<!-- Fields for Exact Date (One-Time or Fixed Recurring) -->
|
||||
<p id="p_exact_date">
|
||||
<label for="holiday_exact_date"><%= l(:field_date, default: 'Date') %></label>
|
||||
<%= f.date_field :exact_date, id: 'holiday_exact_date' %>
|
||||
</p>
|
||||
|
||||
<!-- Fields for Dynamic Date -->
|
||||
<div id="p_dynamic_date" style="display: none;">
|
||||
<p>
|
||||
<label><%= l(:field_dynamic_rule, default: 'Rule') %></label>
|
||||
|
||||
<%= f.select :week_of_month, [
|
||||
[l(:label_week_1st, default: '1st'), 1],
|
||||
[l(:label_week_2nd, default: '2nd'), 2],
|
||||
[l(:label_week_3rd, default: '3rd'), 3],
|
||||
[l(:label_week_4th, default: '4th'), 4],
|
||||
[l(:label_week_last, default: 'Last'), 5]
|
||||
], {}, style: 'width: 80px;' %>
|
||||
|
||||
<%= f.select :day_of_week,
|
||||
[
|
||||
[l(:day_name_sunday, default: 'Sunday'), 0],
|
||||
[l(:day_name_monday, default: 'Monday'), 1],
|
||||
[l(:day_name_tuesday, default: 'Tuesday'), 2],
|
||||
[l(:day_name_wednesday, default: 'Wednesday'), 3],
|
||||
[l(:day_name_thursday, default: 'Thursday'), 4],
|
||||
[l(:day_name_friday, default: 'Friday'), 5],
|
||||
[l(:day_name_saturday, default: 'Saturday'), 6]
|
||||
],
|
||||
{}, style: 'width: 120px;' %>
|
||||
|
||||
<span style="margin: 0 10px;"><%= l(:label_of, default: 'of') %></span>
|
||||
|
||||
<%= f.select :month,
|
||||
Date::MONTHNAMES.compact.each_with_index.map { |m, i| [m, i+1] },
|
||||
{}, style: 'width: 120px;' %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function toggleHolidayFields() {
|
||||
var isRecurring = $('#holiday_is_recurring').is(':checked');
|
||||
var recurrenceType = $('#holiday_recurrence_type').val();
|
||||
|
||||
if (isRecurring) {
|
||||
$('#p_recurrence_type').show();
|
||||
if (recurrenceType === 'dynamic') {
|
||||
$('#p_exact_date').hide();
|
||||
$('#p_dynamic_date').show();
|
||||
} else {
|
||||
$('#p_exact_date').show();
|
||||
$('#p_dynamic_date').hide();
|
||||
}
|
||||
} else {
|
||||
$('#p_recurrence_type').hide();
|
||||
$('#p_exact_date').show();
|
||||
$('#p_dynamic_date').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
toggleHolidayFields();
|
||||
|
||||
$('#holiday_is_recurring, #holiday_recurrence_type').on('change', function() {
|
||||
toggleHolidayFields();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<h2><%= l(:label_holiday_edit, default: 'Edit Holiday') %></h2>
|
||||
|
||||
<%= form_with model: @holiday, url: holiday_path(@holiday), method: :patch, local: true do |f| %>
|
||||
<%= render partial: 'form', locals: { f: f } %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<%= link_to l(:button_cancel), holidays_path, class: 'cancel' %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,55 @@
|
||||
<div class="contextual">
|
||||
<%= link_to 'New Holiday', new_holiday_path, class: 'icon icon-add' %>
|
||||
</div>
|
||||
|
||||
<h2><%= l(:label_holiday_plural, default: 'Holidays') %></h2>
|
||||
|
||||
<% if @holidays.any? %>
|
||||
<div class="autoscroll">
|
||||
<table class="list holidays">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= l(:field_name, default: 'Name') %></th>
|
||||
<th><%= l(:field_type, default: 'Type') %></th>
|
||||
<th><%= l(:field_date, default: 'Date / Rule') %></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @holidays.each do |holiday| %>
|
||||
<tr class="<%= cycle('odd', 'even') %>">
|
||||
<td class="name"><%= holiday.name %></td>
|
||||
|
||||
<td class="type">
|
||||
<% if !holiday.is_recurring %>
|
||||
<span class="badge">One-Time</span>
|
||||
<% else %>
|
||||
<span class="badge" style="color: #169;">Recurring (<%= holiday.recurrence_type.capitalize %>)</span>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<td class="date-rule">
|
||||
<% if !holiday.is_recurring || holiday.recurrence_type == 'fixed' %>
|
||||
<%= holiday.exact_date&.strftime('%B %d, %Y') %>
|
||||
<% elsif holiday.recurrence_type == 'dynamic' %>
|
||||
<%= ['1st', '2nd', '3rd', '4th', 'Last'][holiday.week_of_month - 1] %>
|
||||
<%= Date::DAYNAMES[holiday.day_of_week] %> of
|
||||
<%= Date::MONTHNAMES[holiday.month] %>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
<td class="buttons">
|
||||
<%= link_to l(:button_edit), edit_holiday_path(holiday), class: 'icon icon-edit' %>
|
||||
<%= link_to l(:button_delete), holiday_path(holiday),
|
||||
method: :delete,
|
||||
data: { confirm: l(:text_are_you_sure) },
|
||||
class: 'icon icon-del' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,7 @@
|
||||
<h2><%= l(:label_holiday_new, default: 'New Holiday') %></h2>
|
||||
|
||||
<%= form_with model: @holiday, url: holidays_path, local: true do |f| %>
|
||||
<%= render partial: 'form', locals: { f: f } %>
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<%= link_to l(:button_cancel), holidays_path, class: 'cancel' %>
|
||||
<% end %>
|
||||
Reference in new issue
Block a user