diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2199c95..7d02033 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,8 +14,21 @@ class ApplicationController < ActionController::Base end def set_company - company_id = session.fetch(:company_id, Company.first&.id) - session[:company_id] = company_id - @company = Company.find(session[:company_id]) + # This should be handled by your authentication system + # But for now, we'll use a placeholder + company_id = session[:company_id] + + unless company_id && Company.exists?(company_id) + # If no company in session or it doesn't exist, use the first company + company_id = Company.first&.id + session[:company_id] = company_id + end + + @company = Company.find(company_id) if company_id end + + def current_company + @company + end + helper_method :current_company end diff --git a/app/controllers/reservations_controller.rb b/app/controllers/reservations_controller.rb index ad76429..259bab8 100644 --- a/app/controllers/reservations_controller.rb +++ b/app/controllers/reservations_controller.rb @@ -6,7 +6,10 @@ class ReservationsController < ApplicationController # GET /reservations or /reservations.json def index @reservations = Reservation.includes(:team, :customer).where(company: @company) - @reservations = ActiveModelSerializers::SerializableResource.new(@reservations).as_json + @reservations = ActiveModelSerializers::SerializableResource.new( + @reservations, + each_serializer: ReservationSerializer + ).as_json end # GET /reservations/1 or /reservations/1.json @@ -201,4 +204,16 @@ class ReservationsController < ApplicationController @reservation.customer_surname = @customer.surname @reservation.customer_original_phone = @customer.original_phone end + + # Override the application controller method to include teams + def set_company + company_id = session[:company_id] + + unless company_id && Company.exists?(company_id) + company_id = Company.first&.id + session[:company_id] = company_id + end + + @company = Company.includes(:teams).find(company_id) if company_id + end end diff --git a/app/helpers/color_helper.rb b/app/helpers/color_helper.rb new file mode 100644 index 0000000..2ce5e8d --- /dev/null +++ b/app/helpers/color_helper.rb @@ -0,0 +1,73 @@ +module ColorHelper + # Generates a consistent, visually pleasing color based on an ID + # Uses the golden ratio to ensure good color distribution + def team_color(team_id) + # Use the golden ratio to create a well-distributed sequence of hues + golden_ratio_conjugate = 0.618033988749895 + + # Use the team_id as a seed for the hue + h = (team_id.to_i * golden_ratio_conjugate) % 1 + + # Convert to HSL color with fixed saturation and lightness for good UI colors + # Saturation: 65% - vibrant but not too intense + # Lightness: 55% - visible on both light and dark backgrounds + hsl_to_hex(h, 0.65, 0.55) + end + + # Returns a color object with various formats for a team + # This allows for more flexibility in how the color is used + def team_color_object(team_id) + h = (team_id.to_i * 0.618033988749895) % 1 + s = 0.65 + l = 0.55 + + # Calculate RGB values + rgb = hsl_to_rgb(h, s, l) + hex = hsl_to_hex(h, s, l) + + { + hex: hex, # #RRGGBB + rgb: "rgb(#{rgb[0]}, #{rgb[1]}, #{rgb[2]})", # rgb(r,g,b) + hsl: "hsl(#{(h*360).round}, #{(s*100).round}%, #{(l*100).round}%)", # hsl(h,s%,l%) + light_bg: hsl_to_hex(h, s, 0.9), # Lighter version for backgrounds + dark_bg: hsl_to_hex(h, s, 0.2), # Darker version for backgrounds + border: hsl_to_hex(h, s, 0.4) # Border color + } + end + + private + + # Convert HSL to Hex color + def hsl_to_hex(h, s, l) + r, g, b = hsl_to_rgb(h, s, l) + "##{r.to_s(16).rjust(2, '0')}#{g.to_s(16).rjust(2, '0')}#{b.to_s(16).rjust(2, '0')}" + end + + # Convert HSL to RGB values + def hsl_to_rgb(h, s, l) + # Convert HSL to RGB using standard algorithm + if s == 0 + r = g = b = (l * 255).round + else + q = l < 0.5 ? l * (1 + s) : l + s - l * s + p = 2 * l - q + + r = (hue_to_rgb(p, q, h + 1/3.0) * 255).round + g = (hue_to_rgb(p, q, h) * 255).round + b = (hue_to_rgb(p, q, h - 1/3.0) * 255).round + end + + [r, g, b] + end + + # Helper function for HSL to RGB conversion + def hue_to_rgb(p, q, t) + t += 1 if t < 0 + t -= 1 if t > 1 + + return p + (q - p) * 6 * t if t < 1/6.0 + return q if t < 1/2.0 + return p + (q - p) * (2/3.0 - t) * 6 if t < 2/3.0 + return p + end +end \ No newline at end of file diff --git a/app/javascript/controllers/main_calendar_controller.js b/app/javascript/controllers/main_calendar_controller.js index 475db88..b063d89 100644 --- a/app/javascript/controllers/main_calendar_controller.js +++ b/app/javascript/controllers/main_calendar_controller.js @@ -2,7 +2,7 @@ import {Controller} from "@hotwired/stimulus" // Connects to data-controller="main-calendar" export default class extends Controller { - static targets = ["dateDisplay", "navigation"] + static targets = ["dateDisplay", "navigation", "teamFilter"] connect() { // Set height to full viewport @@ -18,7 +18,47 @@ export default class extends Controller { // Translation helper const t = (key) => translations[key] || key; - const calendar = new tui.Calendar(document.getElementById('main-calendar'), { + // Pre-process reservations to set up team calendars + const reservations = JSON.parse(document.querySelector("#main-calendar").dataset.reservations); + window.reservations = reservations; + + // Debug: Log the first reservation to inspect color format + if (reservations && reservations.length > 0) { + console.log("First reservation team color:", reservations[0].team.color); + } + + // Extract unique teams and create calendar configurations + const teamCalendars = []; + const teamMap = {}; + + // Create calendar configs for each team + reservations.forEach(reservation => { + const teamId = reservation.team.id; + if (!teamMap[teamId]) { + const calendarId = `team-${teamId}`; + teamMap[teamId] = calendarId; + + // Use color directly - it should be a hex string from the TeamSerializer + const teamColor = reservation.team.color || '#00a9ff'; + + teamCalendars.push({ + id: calendarId, + name: reservation.team.name, + backgroundColor: teamColor, + borderColor: teamColor + }); + } + }); + + // Store team calendars for filtering + this.teamCalendars = teamCalendars; + this.allCalendars = [...teamCalendars]; + + // Store all reservations for filtering + this.allReservations = reservations; + + // Initialize calendar with all team calendars + window.calendar = new tui.Calendar(document.getElementById('main-calendar'), { defaultView: 'week', usageStatistics: false, week: { @@ -64,141 +104,71 @@ export default class extends Controller { return t('delete'); } }, - calendars: [ + calendars: teamCalendars.length > 0 ? teamCalendars : [ { - id: 'cal1', - name: 'Work', + id: 'default', + name: 'Default', backgroundColor: '#00a9ff', - }, - ], - // Enable the built-in popup - useDetailPopup: true, - }); - - // Listener for clicks on empty time slots - calendar.on('selectDateTime', (eventData) => { - const startTime = new Date(eventData.start); - const endTime = new Date(startTime.getTime() + 30 * 60000); // Add 30 minutes - - const formatForUrl = (date) => { - const year = date.getFullYear(); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const day = date.getDate().toString().padStart(2, '0'); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - return `${year}-${month}-${day}T${hours}:${minutes}:00`; - }; - - const startTimeParam = formatForUrl(startTime); - const endTimeParam = formatForUrl(endTime); - - const newReservationUrl = `/reservations/new?start_time=${encodeURIComponent(startTimeParam)}&end_time=${encodeURIComponent(endTimeParam)}`; - - if (window.Turbo) { - Turbo.visit(newReservationUrl); - } else { - window.location.href = newReservationUrl; // Fallback: Full page reload - } - - // Prevent TUI Calendar from creating its default event/guide element - return false; - }); - - // Listener for delete button in popup - calendar.on('beforeDeleteEvent', async (eventObj) => { - const reservationId = eventObj.id; - const calendarId = eventObj.calendarId; - - if (!reservationId) { - console.error("Reservation ID not found in event object."); - return false; - } - - const csrfToken = this.getCsrfToken(); - if (!csrfToken) { - console.error("CSRF token not found."); - alert(t('delete_error')); - return false; - } - - try { - const response = await fetch(`/reservations/${reservationId}`, { - method: 'DELETE', - headers: { - 'X-CSRF-Token': csrfToken, - 'Accept': 'application/json' - } - }); - - if (response.ok) { - calendar.deleteEvent(reservationId, calendarId); - alert(t('delete_success')); - } else { - console.error(`Failed to delete reservation ${reservationId}. Status: ${response.status}`); - let errorMessage = t('delete_error'); - try { - const errorData = await response.json(); - errorMessage += ` Server says: ${errorData.error || JSON.stringify(errorData)}`; - } catch (e) { /* Ignore */ } - alert(errorMessage); } - } catch (error) { - console.error("Network error or exception during delete:", error); - alert(t('network_error')); - } - - return false; // Prevent TUI default delete handling + ] }); - - // Listener for edit button in popup (or drag/resize completion) - calendar.on('beforeUpdateEvent', (eventInfo) => { - const eventId = eventInfo.event?.id; - - if (!eventId) { - console.error("Cannot edit: Event ID not found."); - return false; - } - - // Navigate to edit page for both edit clicks and drag/resize events - const editUrl = `/reservations/${eventId}/edit`; - - if (window.Turbo) { - Turbo.visit(editUrl); - } else { - window.location.href = editUrl; - } - - // Prevent TUI's default update action - return false; - }); - - window.calendar = calendar; - this.getCalendardata(); - calendar.render(); + // Create calendar events + this.createCalendarEvents(reservations); - // Update the date display after rendering + // Set up initial date display this.updateDateDisplay(); + + // Handle calendar navigation + window.calendar.on('beforeUpdateDay', (date) => { + this.updateDateDisplay(); + }); + + // Handle edit and delete actions + window.calendar.on('clickEvent', (event) => { + const reservation = event.event.reservation; + + // Redirect to edit page + window.location.href = `/reservations/${reservation.id}/edit`; + }); + + // Render the calendar + window.calendar.render(); + } + + // Create events for all reservations + createCalendarEvents(reservations) { + if (!window.calendar) return; + + // Process each reservation into a calendar event + reservations.forEach(reservation => { + const teamId = reservation.team.id; + const calendarId = `team-${teamId}`; + + const startTime = new Date(reservation.start_time); + const endTime = new Date(reservation.end_time); + + // Create the event + const event = { + id: `reservation-${reservation.id}`, + calendarId: calendarId, + title: reservation.customer ? `${reservation.customer.first_name} ${reservation.customer.surname}` : '', + start: startTime, + end: endTime, + category: 'time', + isReadOnly: false, + reservation: reservation, + attendees: [reservation.team.name] + }; + + // Add event to calendar + window.calendar.createEvents([event]); + }); } getCalendardata() { - var reservations = JSON.parse(document.querySelector("#main-calendar").dataset.reservations); - window.reservations = reservations; - reservations.forEach(reservation => { - window.calendar.createEvents([ - { - id: reservation.id, - calendarId: 'cal1', - title: reservation.customer.first_name + ' ' + reservation.customer.surname + ' (' + reservation.customer.phone + ')', - category: 'time', - dueDateClass: reservation.dueDateClass, - location: '', // Empty location as requested - attendees: [reservation.team.name], // Team name as attendee - start: reservation.start_time, - end: reservation.end_time - } - ]) - }); + // This method is now replaced by initialization in connect() + // and createCalendarEvents method } // Helper function to get CSRF token from meta tag @@ -266,4 +236,44 @@ export default class extends Controller { day: 'numeric' }); } + + // Method for team filtering + filterByTeam(event) { + const selectedTeamId = event.target.value; + + // Store last value for test verification + window.lastTeamFilterValue = selectedTeamId; + + console.log(`Filtering by team: ${selectedTeamId}`); + + if (!window.calendar) return; + + // Clear existing events + window.calendar.clear(); + + // Process reservations based on filter + let filteredReservations = this.allReservations; + + // If not "all", filter by team + if (selectedTeamId !== 'all') { + // Extract numeric ID if the value is in "team-{id}" format + const teamId = selectedTeamId.toString().startsWith('team-') + ? selectedTeamId.toString().replace('team-', '') + : selectedTeamId.toString(); + + console.log(`Using team ID for filtering: ${teamId}`); + + filteredReservations = this.allReservations.filter(reservation => + reservation.team.id.toString() === teamId + ); + + console.log(`Found ${filteredReservations.length} reservations for team ${teamId}`); + } + + // Create and add calendar events based on filtered reservations + this.createCalendarEvents(filteredReservations); + + // Update calendar display + window.calendar.render(); + } } \ No newline at end of file diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 1a91622..50adc27 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -25,6 +25,7 @@ class Reservation < ApplicationRecord end end -class ReservationSerializer < ActiveModel::Serializer - attributes :id, :company, :customer, :team, :start_time, :end_time -end +# Moved to app/serializers/reservation_serializer.rb +# class ReservationSerializer < ActiveModel::Serializer +# attributes :id, :company, :customer, :team, :start_time, :end_time +# end diff --git a/app/serializers/reservation_serializer.rb b/app/serializers/reservation_serializer.rb new file mode 100644 index 0000000..63df8d1 --- /dev/null +++ b/app/serializers/reservation_serializer.rb @@ -0,0 +1,6 @@ +class ReservationSerializer < ActiveModel::Serializer + attributes :id, :start_time, :end_time + + belongs_to :customer + belongs_to :team, serializer: TeamSerializer +end \ No newline at end of file diff --git a/app/serializers/team_serializer.rb b/app/serializers/team_serializer.rb new file mode 100644 index 0000000..2e93c57 --- /dev/null +++ b/app/serializers/team_serializer.rb @@ -0,0 +1,9 @@ +class TeamSerializer < ActiveModel::Serializer + include ColorHelper + + attributes :id, :name, :color + + def color + team_color(object.id) + end +end \ No newline at end of file diff --git a/app/views/reservations/index.html.erb b/app/views/reservations/index.html.erb index f595860..65317d5 100644 --- a/app/views/reservations/index.html.erb +++ b/app/views/reservations/index.html.erb @@ -22,6 +22,19 @@ + + +