Merge branch '9-napraviti-lako-filtriranje-timova-na-kalendaru' into 'master'
Merge branch '8-podrska-za-boje-na-kalendaru' into 'master' Closes #8 and #9 See merge request kbr4/zsterminator!7
This commit was merged in pull request #21.
This commit is contained in:
@@ -14,8 +14,21 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_company
|
def set_company
|
||||||
company_id = session.fetch(:company_id, Company.first&.id)
|
# This should be handled by your authentication system
|
||||||
session[:company_id] = company_id
|
# But for now, we'll use a placeholder
|
||||||
@company = Company.find(session[:company_id])
|
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
|
end
|
||||||
|
|
||||||
|
def current_company
|
||||||
|
@company
|
||||||
|
end
|
||||||
|
helper_method :current_company
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -204,4 +204,16 @@ class ReservationsController < ApplicationController
|
|||||||
@reservation.customer_surname = @customer.surname
|
@reservation.customer_surname = @customer.surname
|
||||||
@reservation.customer_original_phone = @customer.original_phone
|
@reservation.customer_original_phone = @customer.original_phone
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Controller} from "@hotwired/stimulus"
|
|||||||
// Connects to data-controller="main-calendar"
|
// Connects to data-controller="main-calendar"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["dateDisplay", "navigation"]
|
static targets = ["dateDisplay", "navigation", "teamFilter"]
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
// Set height to full viewport
|
// 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
|
// 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',
|
defaultView: 'week',
|
||||||
usageStatistics: false,
|
usageStatistics: false,
|
||||||
week: {
|
week: {
|
||||||
@@ -103,140 +110,60 @@ export default class extends Controller {
|
|||||||
name: 'Default',
|
name: 'Default',
|
||||||
backgroundColor: '#00a9ff',
|
backgroundColor: '#00a9ff',
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
// Enable the built-in popup
|
|
||||||
useDetailPopup: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listener for clicks on empty time slots
|
// Create calendar events
|
||||||
calendar.on('selectDateTime', (eventData) => {
|
this.createCalendarEvents(reservations);
|
||||||
const startTime = new Date(eventData.start);
|
|
||||||
const endTime = new Date(startTime.getTime() + 30 * 60000); // Add 30 minutes
|
|
||||||
|
|
||||||
const formatForUrl = (date) => {
|
// Set up initial date display
|
||||||
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);
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
|
|
||||||
// Update the date display after rendering
|
|
||||||
this.updateDateDisplay();
|
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
|
// Create events for all reservations
|
||||||
createCalendarEvents(reservations, teamMap) {
|
createCalendarEvents(reservations) {
|
||||||
// Create events with their team's calendar ID
|
if (!window.calendar) return;
|
||||||
const events = reservations.map(reservation => {
|
|
||||||
|
// Process each reservation into a calendar event
|
||||||
|
reservations.forEach(reservation => {
|
||||||
const teamId = reservation.team.id;
|
const teamId = reservation.team.id;
|
||||||
const calendarId = teamMap[teamId] || 'default';
|
const calendarId = `team-${teamId}`;
|
||||||
|
|
||||||
return {
|
const startTime = new Date(reservation.start_time);
|
||||||
id: reservation.id,
|
const endTime = new Date(reservation.end_time);
|
||||||
|
|
||||||
|
// Create the event
|
||||||
|
const event = {
|
||||||
|
id: `reservation-${reservation.id}`,
|
||||||
calendarId: calendarId,
|
calendarId: calendarId,
|
||||||
title: reservation.customer.first_name + ' ' + reservation.customer.surname + ' (' + reservation.customer.phone + ')',
|
title: reservation.customer ? `${reservation.customer.first_name} ${reservation.customer.surname}` : '',
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
category: 'time',
|
category: 'time',
|
||||||
dueDateClass: reservation.dueDateClass,
|
isReadOnly: false,
|
||||||
location: '', // Empty location as requested
|
reservation: reservation,
|
||||||
attendees: [reservation.team.name], // Team name as attendee
|
attendees: [reservation.team.name]
|
||||||
start: reservation.start_time,
|
|
||||||
end: reservation.end_time
|
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
window.calendar.createEvents(events);
|
// Add event to calendar
|
||||||
|
window.calendar.createEvents([event]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCalendardata() {
|
getCalendardata() {
|
||||||
@@ -309,4 +236,44 @@ export default class extends Controller {
|
|||||||
day: 'numeric'
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,19 @@
|
|||||||
</button>
|
</button>
|
||||||
<span data-main-calendar-target="dateDisplay" class="ml-3 font-medium"></span>
|
<span data-main-calendar-target="dateDisplay" class="ml-3 font-medium"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Team Filter Dropdown -->
|
||||||
|
<div class="ml-6">
|
||||||
|
<label for="team-filter" class="mr-2 font-medium"><%= t('.filter_by_team') %>:</label>
|
||||||
|
<select id="team-filter" data-main-calendar-target="teamFilter" data-action="change->main-calendar#filterByTeam" class="rounded-md border-gray-300 shadow-sm px-3 py-1 bg-white">
|
||||||
|
<option value="all"><%= t('.all_teams') %></option>
|
||||||
|
<% @company.teams.each do |team| %>
|
||||||
|
<option value="<%= team.id %>" style="background-color: <%= team_color(team.id) %>; color: #000000; padding-left: 10px;">
|
||||||
|
<%= team.name %>
|
||||||
|
</option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= 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" %>
|
<%= 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" %>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +51,8 @@
|
|||||||
delete_error: t('reservations.calendar.delete_error'),
|
delete_error: t('reservations.calendar.delete_error'),
|
||||||
network_error: t('reservations.calendar.network_error'),
|
network_error: t('reservations.calendar.network_error'),
|
||||||
edit: t('reservations.calendar.edit'),
|
edit: t('reservations.calendar.edit'),
|
||||||
delete: t('reservations.calendar.delete')
|
delete: t('reservations.calendar.delete'),
|
||||||
|
all_teams: t('reservations.calendar.all_teams')
|
||||||
}.to_json
|
}.to_json
|
||||||
%>
|
%>
|
||||||
<%= tag.div nil,
|
<%= tag.div nil,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ bs:
|
|||||||
today: "Danas"
|
today: "Danas"
|
||||||
next: "Sljedeća »"
|
next: "Sljedeća »"
|
||||||
new_reservation: "Nova rezervacija" # TODO: Translate to Bosnian
|
new_reservation: "Nova rezervacija" # TODO: Translate to Bosnian
|
||||||
|
filter_by_team: "Filtriraj po timu"
|
||||||
|
all_teams: "Svi timovi"
|
||||||
calendar:
|
calendar:
|
||||||
hours: "sati"
|
hours: "sati"
|
||||||
delete_confirm: "Jeste li sigurni da želite izbrisati ovu rezervaciju?"
|
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."
|
network_error: "Greška prilikom brisanja rezervacije zbog problema s mrežom ili skriptom."
|
||||||
edit: "Uredi"
|
edit: "Uredi"
|
||||||
delete: "Obriši"
|
delete: "Obriši"
|
||||||
|
all_teams: "Svi timovi"
|
||||||
# Keys for form and edit/new pages
|
# Keys for form and edit/new pages
|
||||||
edit:
|
edit:
|
||||||
title: "Uređivanje rezervacije"
|
title: "Uređivanje rezervacije"
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ en:
|
|||||||
today: "Today"
|
today: "Today"
|
||||||
next: "Next »"
|
next: "Next »"
|
||||||
new_reservation: "New reservation"
|
new_reservation: "New reservation"
|
||||||
|
filter_by_team: "Filter by team"
|
||||||
|
all_teams: "All teams"
|
||||||
calendar:
|
calendar:
|
||||||
hours: "hours"
|
hours: "hours"
|
||||||
delete_confirm: "Are you sure you want to delete this reservation?"
|
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."
|
network_error: "Error deleting reservation due to a network or script issue."
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
|
all_teams: "All teams"
|
||||||
edit:
|
edit:
|
||||||
title: "Editing reservation"
|
title: "Editing reservation"
|
||||||
show: "Show this reservation"
|
show: "Show this reservation"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ require_relative '../config/environment'
|
|||||||
# Prevent database truncation if the environment is production
|
# Prevent database truncation if the environment is production
|
||||||
abort("The Rails environment is running in production mode!") if Rails.env.production?
|
abort("The Rails environment is running in production mode!") if Rails.env.production?
|
||||||
require 'rspec/rails'
|
require 'rspec/rails'
|
||||||
|
require 'capybara/rails'
|
||||||
|
require 'capybara/rspec'
|
||||||
# Add additional requires below this line. Rails is not loaded until this point!
|
# Add additional requires below this line. Rails is not loaded until this point!
|
||||||
|
|
||||||
# Requires supporting ruby files with custom matchers and macros, etc, in
|
# Requires supporting ruby files with custom matchers and macros, etc, in
|
||||||
@@ -61,4 +63,33 @@ RSpec.configure do |config|
|
|||||||
config.filter_rails_from_backtrace!
|
config.filter_rails_from_backtrace!
|
||||||
# arbitrary gems may also be filtered via:
|
# arbitrary gems may also be filtered via:
|
||||||
# config.filter_gems_from_backtrace("gem name")
|
# 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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user