diff --git a/app/controllers/customer_appointments_controller.rb b/app/controllers/customer_appointments_controller.rb index a5fd077..acb80cf 100644 --- a/app/controllers/customer_appointments_controller.rb +++ b/app/controllers/customer_appointments_controller.rb @@ -93,6 +93,7 @@ class CustomerAppointmentsController < ApplicationController :end_date, :estimated_hours, :estimate_id, + :duration ) end diff --git a/app/views/common/_calendar.html.erb b/app/views/common/_calendar.html.erb index 5724ee5..dc1116c 100644 --- a/app/views/common/_calendar.html.erb +++ b/app/views/common/_calendar.html.erb @@ -1,3 +1,43 @@ +<% + # Get Redmine's configured non-working week days (e.g., ["6", "7"] for Sat/Sun) + non_working_days = Setting.non_working_week_days || [] + + # Safely collect all unique CustomerAppointments across the entire calendar date range + all_appointments = (calendar.startdt..calendar.enddt).flat_map { |date| + calendar.events_on(date) + }.select { |e| e.is_a?(CustomerAppointment) }.uniq + + # Lambda to determine if an appointment should render on a specific calendar day + appointment_occurs_on = lambda do |appointment, current_day| + return false if appointment.start_date.nil? + + start_date = appointment.start_date.to_date + target_date = current_day.to_date + + return false if target_date < start_date + # Skip rendering on Redmine non-working days + return false if non_working_days.include?(target_date.cwday.to_s) + + 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) + working_days_count += 1 + end + + if date_cursor == target_date + return working_days_count <= duration + end + + date_cursor += 1 # Pure Ruby Date increment stays a Date object + end + + false + end +%> + <%= form_tag({}, data: { cm_url: issues_context_menu_path }) do %> <%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %> @@ -31,7 +71,15 @@ title: l(:label_appointment_new, default: 'New Appointment') %>

- <% calendar.events_on(day).each do |event| %> + <% + # Non-appointment events (Issues, Versions, etc.) + standard_events = calendar.events_on(day).reject { |e| e.is_a?(CustomerAppointment) } + + # Appointments matching duration on this working day + matching_appointments = all_appointments.select { |app| appointment_occurs_on.call(app, day) } + %> + + <% (standard_events + matching_appointments).each do |event| %> <% if event.is_a?(Issue) %> diff --git a/app/views/customer_appointments/_tooltip.html.erb b/app/views/customer_appointments/_tooltip.html.erb index 566423c..94d897d 100644 --- a/app/views/customer_appointments/_tooltip.html.erb +++ b/app/views/customer_appointments/_tooltip.html.erb @@ -4,28 +4,28 @@

- <%= appointment.status %> + <%= appointment.status.to_s.humanize %>

<% if appointment.customer %> - - + + <% end %> <% if appointment.vehicle %> - - + + <% end %> <% if appointment.user %> - + - + - + - + - + + + + + +
<%= l(:label_customer) rescue "Customer" %><%= link_to appointment.customer %><%= l(:label_customer, default: "Customer") %><%= link_to appointment.customer.name, appointment.customer %>
Vehicle<%= link_to appointment.vehicle %><%= l(:label_vehicle, default: "Vehicle") %><%= link_to appointment.vehicle.name, appointment.vehicle %>
Technician<%= l(:label_technician, default: "Technician") %> <%= avatar(appointment.user, size: 16) %> <%= appointment.user.name %> @@ -42,7 +42,7 @@ <% if appointment.estimate %>
Estimate<%= l(:label_estimate, default: "Estimate") %> <%= link_to "##{appointment.estimate.id}", appointment.estimate %> @@ -51,20 +51,25 @@ <% end %>
Start<%= l(:label_start_date) %> <%= format_date(appointment.start_date) %>
Finish<%= l(:label_end_date) %> <%= format_date(appointment.end_date || appointment.start_date) %>
Estimated Time<%= l(:label_estimated_hours) %> <%= number_with_precision(appointment.estimated_hours, precision: 1) %> hrs
<%= l(:label_duration) %><%= number_with_precision(appointment.duration, precision: 1) %> days
<% if appointment.description.present? %> @@ -80,7 +85,7 @@

- <%= link_to "Open Appointment", + <%= link_to l(:label_open_appointment, default: "Open Appointment"), customer_appointment_path(appointment), class: "icon icon-edit" %>

\ No newline at end of file diff --git a/assets/stylesheets/calendar.js b/assets/stylesheets/calendar.js new file mode 100644 index 0000000..255cacf --- /dev/null +++ b/assets/stylesheets/calendar.js @@ -0,0 +1,22 @@ +document.addEventListener('DOMContentLoaded', function() { + document.addEventListener('click', function(e) { + const clickedBlock = e.target.closest('.appointment-block'); + const clickedLink = e.target.closest('a'); + + // Allow standard links (like subject title or avatar links) to navigate normally + if (clickedLink) return; + + // Close any other open tooltips across the calendar + document.querySelectorAll('.appointment-block.open').forEach(function(block) { + if (block !== clickedBlock) { + block.classList.remove('open'); + } + }); + + // Toggle the clicked appointment block's tooltip + if (clickedBlock) { + e.stopPropagation(); + clickedBlock.classList.toggle('open'); + } + }); +}); \ No newline at end of file diff --git a/assets/stylesheets/customer_appointments.css b/assets/stylesheets/customer_appointments.css index 9e53aec..a8cd5ea 100644 --- a/assets/stylesheets/customer_appointments.css +++ b/assets/stylesheets/customer_appointments.css @@ -11,8 +11,16 @@ border-radius: 3px; font-size: 10px; line-height: 1.2; - overflow: visible !important; /* CRITICAL: Allows tooltip popover to extend outside */ + overflow: visible !important; /* Allows tooltip popover to extend outside */ max-width: 100%; + cursor: pointer; /* Cue for clickability */ + transition: transform 0.1s ease, box-shadow 0.1s ease; +} + +/* Hover Cue */ +.appointment-block:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12); } /* Left Indicator Bar */ @@ -37,13 +45,13 @@ .appointment-block.cancelled .appointment-bar { background-color: #d9534f; } .appointment-block.cancelled { background: #fdf2f2; border-color: #f8c8c8; } -/* Card Content Area - Contains text overflow inside the card */ +/* Card Content Area */ .appointment-card { display: flex; flex-direction: column; flex-grow: 1; min-width: 0; - overflow: hidden; /* Truncates long text cleanly inside the card */ + overflow: hidden; /* Truncates long text cleanly inside card */ } .appointment-header { @@ -90,14 +98,15 @@ min-width: 0; } -/* Hover Effect */ -.appointment-block:hover { - z-index: 999; +/* Elevated Stack Order When Opened */ +.appointment-block.open { + z-index: 1000 !important; } -/* Redmine Tooltip Positioning & Visibility Fixes */ -.appointment-block .tip { - display: none; +/* Redmine Tooltip Popover (Hidden by Default & on Hover) */ +.appointment-block .tip, +.appointment-block:hover .tip { + display: none !important; /* Overrides built-in hover triggers */ position: absolute; top: 100%; left: 0; @@ -106,11 +115,74 @@ background: #ffffff; border: 1px solid #ccc; border-radius: 4px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); white-space: normal; z-index: 10000 !important; + cursor: default; } -.appointment-block:hover .tip { - display: block; +/* Display Tooltip ONLY on Click (.open class) */ +.appointment-block.open .tip, +.appointment-block.open:hover .tip { + display: block !important; +} + +/* Inner Tooltip Formatting */ +.appointment-block .tip h3 { + margin: 0 0 4px 0; + font-size: 13px; + line-height: 1.3; +} + +.appointment-block .tip .subtitle { + margin: 0 0 8px 0; + font-size: 11px; + color: #666; + text-transform: capitalize; +} + +.appointment-block .tip table.attributes { + width: 100%; + border-collapse: collapse; + font-size: 11px; + margin-bottom: 4px; +} + +.appointment-block .tip table.attributes th { + text-align: left; + padding: 3px 6px 3px 0; + color: #555; + font-weight: bold; + width: 35%; + vertical-align: top; +} + +.appointment-block .tip table.attributes td { + padding: 3px 0; + color: #222; + vertical-align: top; +} + +.appointment-block .tip table.attributes img.gravatar { + vertical-align: middle; + margin-right: 4px; +} + +.appointment-block .tip hr { + border: 0; + border-top: 1px solid #e0e0e0; + margin: 8px 0; +} + +.appointment-block .tip .description { + max-height: 120px; + overflow-y: auto; + font-size: 11px; + color: #444; + line-height: 1.4; +} + +.appointment-block .tip .buttons { + margin: 0; + text-align: right; } \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 09f1ad9..db7e679 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -11,15 +11,19 @@ # English strings go here for Rails i18n # Usage I18n.t(:label) en: - label_event: "Event" - permission_view_customer_appointments: View Customer Appointments - permission_manage_customer_appointments: Manage Customer Appointments - label_customer_calendar: Customer Calendar - label_customer_appointment: Customer Appointment - label_duration: Duration - field_customer_name: Customer - field_vehicle: Vehicle - field_phone: Phone - field_license_plate: License Plate are_you_sure: "Are you sure?" + field_customer_name: Customer + field_license_plate: License Plate + field_phone: Phone + field_vehicle: Vehicle + label_customer_appointment: Customer Appointment + label_customer_calendar: Customer Calendar + label_duration: Duration + label_end_date: End Date + label_estimated_hours: Estimated Time + label_event: "Event" + label_open_appointment: Open Appointment + label_start_date: Start Date + permission_manage_customer_appointments: Manage Customer Appointments + permission_view_customer_appointments: View Customer Appointments \ No newline at end of file diff --git a/lib/redmine_qbo_calendar/hooks/view_hook_listener.rb b/lib/redmine_qbo_calendar/hooks/view_hook_listener.rb index 3ac7bbd..c6693c2 100644 --- a/lib/redmine_qbo_calendar/hooks/view_hook_listener.rb +++ b/lib/redmine_qbo_calendar/hooks/view_hook_listener.rb @@ -17,6 +17,7 @@ module RedmineQboCalendar def view_layouts_base_html_head(context = {}) safe_join([ javascript_include_tag("customer_appointments", plugin: :redmine_qbo_calendar), + javascript_include_tag("calendar", plugin: :redmine_qbo_calendar), stylesheet_link_tag('customer_appointments', plugin: :redmine_qbo_calendar) ]) end