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 3494c61..259bab8 100644 --- a/app/controllers/reservations_controller.rb +++ b/app/controllers/reservations_controller.rb @@ -204,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/javascript/controllers/main_calendar_controller.js b/app/javascript/controllers/main_calendar_controller.js index 69e73b7..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 @@ -50,8 +50,15 @@ export default class extends Controller { } }); + // 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 - const calendar = new tui.Calendar(document.getElementById('main-calendar'), { + window.calendar = new tui.Calendar(document.getElementById('main-calendar'), { defaultView: 'week', usageStatistics: false, week: { @@ -103,140 +110,60 @@ export default class extends Controller { 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; - // Create events for all reservations - this.createCalendarEvents(reservations, teamMap); + // Create calendar events + this.createCalendarEvents(reservations); - calendar.render(); - - // 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, teamMap) { - // Create events with their team's calendar ID - const events = reservations.map(reservation => { - const teamId = reservation.team.id; - const calendarId = teamMap[teamId] || 'default'; - - return { - id: reservation.id, - calendarId: calendarId, - 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 - }; - }); + createCalendarEvents(reservations) { + if (!window.calendar) return; - window.calendar.createEvents(events); + // 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() { @@ -309,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/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 @@ + + +
+ + +
<%= link_to t('.new_reservation'), new_reservation_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium ml-auto" %> @@ -38,7 +51,8 @@ delete_error: t('reservations.calendar.delete_error'), network_error: t('reservations.calendar.network_error'), edit: t('reservations.calendar.edit'), - delete: t('reservations.calendar.delete') + delete: t('reservations.calendar.delete'), + all_teams: t('reservations.calendar.all_teams') }.to_json %> <%= tag.div nil, diff --git a/config/locales/bs.yml b/config/locales/bs.yml index 7cde88c..e3fc467 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -7,6 +7,8 @@ bs: today: "Danas" next: "Sljedeća »" new_reservation: "Nova rezervacija" # TODO: Translate to Bosnian + filter_by_team: "Filtriraj po timu" + all_teams: "Svi timovi" calendar: hours: "sati" delete_confirm: "Jeste li sigurni da želite izbrisati ovu rezervaciju?" @@ -15,6 +17,7 @@ bs: network_error: "Greška prilikom brisanja rezervacije zbog problema s mrežom ili skriptom." edit: "Uredi" delete: "Obriši" + all_teams: "Svi timovi" # Keys for form and edit/new pages edit: title: "Uređivanje rezervacije" diff --git a/config/locales/en.yml b/config/locales/en.yml index 28b7ce0..6a0ea99 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -52,6 +52,8 @@ en: today: "Today" next: "Next »" new_reservation: "New reservation" + filter_by_team: "Filter by team" + all_teams: "All teams" calendar: hours: "hours" delete_confirm: "Are you sure you want to delete this reservation?" @@ -60,6 +62,7 @@ en: network_error: "Error deleting reservation due to a network or script issue." edit: "Edit" delete: "Delete" + all_teams: "All teams" edit: title: "Editing reservation" show: "Show this reservation" diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a42a83c..d125796 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -5,6 +5,8 @@ require_relative '../config/environment' # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' +require 'capybara/rails' +require 'capybara/rspec' # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in @@ -61,4 +63,33 @@ RSpec.configure do |config| config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + # Include FactoryBot methods + config.include FactoryBot::Syntax::Methods + + # Configure Capybara for JavaScript tests + Capybara.javascript_driver = :selenium_chrome_headless + + # Use DatabaseCleaner for feature specs + config.use_transactional_fixtures = false + + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each, js: true) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end end