Merge branch '5-napraviti-da-kreiranje-radi-bez-dugmeta-new-reservation' into 'master'
Resolve "Napraviti da kreiranje radi bez dugmeta "new reservation"" Closes #5 and #2 See merge request kbr4/zsterminator!3
This commit was merged in pull request #17.
This commit is contained in:
@@ -14,9 +14,22 @@ class ReservationsController < ApplicationController
|
||||
|
||||
# GET /reservations/new
|
||||
def new
|
||||
@reservation = Reservation.new
|
||||
@reservation.team = @company.teams.first
|
||||
@customers = @company.customers
|
||||
# logger.debug "--- Reservations#new --- Params received: #{params.inspect}"
|
||||
|
||||
# Use Time.zone.parse to interpret times within the application's configured timezone
|
||||
start_time_param = params[:start_time]
|
||||
end_time_param = params[:end_time]
|
||||
# logger.debug "--- Reservations#new --- Start Param: #{start_time_param}, End Param: #{end_time_param}"
|
||||
|
||||
start_time = start_time_param ? Time.zone.parse(start_time_param) : Time.current
|
||||
end_time = end_time_param ? Time.zone.parse(end_time_param) : Time.current + 30.minutes
|
||||
# logger.debug "--- Reservations#new --- Parsed Start: #{start_time}, Parsed End: #{end_time}"
|
||||
|
||||
@reservation = Reservation.new(start_time: start_time, end_time: end_time)
|
||||
# logger.debug "--- Reservations#new --- Reservation object initialized: #{@reservation.attributes.inspect}"
|
||||
|
||||
@reservation.team = @company.teams.first # Assign default team
|
||||
@customers = @company.customers # Preload customers for potential dropdown
|
||||
end
|
||||
|
||||
# GET /reservations/1/edit
|
||||
@@ -28,8 +41,9 @@ class ReservationsController < ApplicationController
|
||||
reservation_params.except(:customer_id, :customer_birth_year)
|
||||
)
|
||||
|
||||
# Find or create customer
|
||||
# Find or create customer based on submitted attributes
|
||||
find_or_create_customer
|
||||
# Associate the reservation with the found/created customer's primary key attributes
|
||||
assign_customer_to_reservation
|
||||
|
||||
if @reservation.save
|
||||
@@ -39,6 +53,7 @@ class ReservationsController < ApplicationController
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
# If customer creation/validation fails
|
||||
@reservation.errors.add(:base, "Failed to save customer: #{e.message}")
|
||||
@customers = @company.customers
|
||||
render :new, status: :unprocessable_entity
|
||||
@@ -46,15 +61,55 @@ class ReservationsController < ApplicationController
|
||||
|
||||
# PATCH/PUT /reservations/1 or /reservations/1.json
|
||||
def update
|
||||
# Separate reservation attributes from customer attributes
|
||||
customer_attrs = build_customer_attributes # Use existing helper
|
||||
reservation_attrs = reservation_params.except(
|
||||
:customer_id, :customer_first_name, :customer_surname,
|
||||
:customer_original_phone, :customer_birth_year
|
||||
)
|
||||
|
||||
# Find the customer identified by the submitted name/phone
|
||||
@customer = Customer.find_by(
|
||||
first_name: customer_attrs[:first_name],
|
||||
surname: customer_attrs[:surname],
|
||||
original_phone: customer_attrs[:original_phone],
|
||||
company_id: @company.id
|
||||
)
|
||||
|
||||
# Update customer phone/birthyear if found (identifiers assumed immutable here)
|
||||
customer_updated = @customer ? @customer.update(customer_attrs.slice(:phone, :birthyear)) : false
|
||||
|
||||
# Assign the correct customer foreign keys to the reservation attributes
|
||||
if @customer
|
||||
reservation_attrs[:customer_first_name] = @customer.first_name
|
||||
reservation_attrs[:customer_surname] = @customer.surname
|
||||
reservation_attrs[:customer_original_phone] = @customer.original_phone
|
||||
else
|
||||
# Fall back to original keys if form customer wasn't found (e.g., identifiers changed)
|
||||
# Consider adding an error or different handling if customer *must* be found.
|
||||
reservation_attrs[:customer_first_name] = @reservation.customer_first_name
|
||||
reservation_attrs[:customer_surname] = @reservation.customer_surname
|
||||
reservation_attrs[:customer_original_phone] = @reservation.customer_original_phone
|
||||
end
|
||||
|
||||
reservation_updated = @reservation.update(reservation_attrs)
|
||||
|
||||
respond_to do |format|
|
||||
if @reservation.update(reservation_params)
|
||||
# Check if reservation update was successful
|
||||
# (We might ignore customer_updated status for now, or add more complex checks)
|
||||
if reservation_updated
|
||||
format.html { redirect_to reservation_url(@reservation), notice: t('.reservation_updated') }
|
||||
format.json { render :show, status: :ok, location: @reservation }
|
||||
else
|
||||
@customers = @company.customers # Reload for form
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @reservation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e # Catch potential customer update errors
|
||||
@reservation.errors.add(:base, "Failed to save customer: #{e.message}") if @customer&.invalid?
|
||||
@customers = @company.customers
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
# DELETE /reservations/1 or /reservations/1.json
|
||||
@@ -76,17 +131,17 @@ class ReservationsController < ApplicationController
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def reservation_params
|
||||
# Permit composite key if form uses it, otherwise permit individual fields
|
||||
# params.require(:reservation).permit(:customer_composite_key, ...)
|
||||
params.require(:reservation).permit(
|
||||
:customer_id,
|
||||
:team_id,
|
||||
:start_time,
|
||||
:end_time,
|
||||
:title,
|
||||
:description,
|
||||
:customer_first_name,
|
||||
:customer_surname,
|
||||
:customer_original_phone,
|
||||
:customer_birth_year # Keep this in permitted params
|
||||
:customer_birth_year,
|
||||
:customer_id # Allow this if select still sends it sometimes
|
||||
)
|
||||
end
|
||||
|
||||
@@ -94,6 +149,7 @@ class ReservationsController < ApplicationController
|
||||
action_name == 'index' ? 'calendar' : 'application'
|
||||
end
|
||||
|
||||
# Finds or creates customer based on submitted fields
|
||||
def find_or_create_customer
|
||||
customer_params = build_customer_attributes
|
||||
|
||||
@@ -104,33 +160,43 @@ class ReservationsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts customer attributes from reservation form parameters
|
||||
def build_customer_attributes
|
||||
{
|
||||
first_name: params[:reservation][:customer_first_name],
|
||||
surname: params[:reservation][:customer_surname],
|
||||
original_phone: params[:reservation][:customer_original_phone],
|
||||
phone: params[:reservation][:customer_original_phone],
|
||||
phone: params[:reservation][:customer_original_phone], # Assuming phone is same as original_phone for now
|
||||
birthyear: params[:reservation][:customer_birth_year],
|
||||
company_id: @company.id
|
||||
}
|
||||
end
|
||||
|
||||
# Checks if the submitted customer ID indicates a new customer
|
||||
def new_customer?
|
||||
params[:reservation][:customer_id].present? &&
|
||||
params[:reservation][:customer_id].end_with?('__new')
|
||||
# Check based on the specific value format used by TomSelect create function
|
||||
params[:reservation][:customer_composite_key]&.end_with?('__new') ||
|
||||
params[:reservation][:customer_id]&.end_with?('__new') # Fallback if old key is used
|
||||
end
|
||||
|
||||
# Finds customer by composite key or creates them
|
||||
def find_or_initialize_customer(attributes)
|
||||
# Find using the composite key fields
|
||||
Customer.find_or_create_by!(
|
||||
first_name: attributes[:first_name],
|
||||
surname: attributes[:surname],
|
||||
original_phone: attributes[:original_phone]
|
||||
# company_id: attributes[:company_id] # Scope to company if needed
|
||||
) do |customer|
|
||||
customer.assign_attributes(attributes)
|
||||
# Assign other attributes only if creating
|
||||
customer.assign_attributes(attributes.slice(:phone, :birthyear, :company_id))
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the foreign key fields on the reservation based on the found/created customer
|
||||
def assign_customer_to_reservation
|
||||
return unless @customer # Guard clause
|
||||
|
||||
@reservation.customer_first_name = @customer.first_name
|
||||
@reservation.customer_surname = @customer.surname
|
||||
@reservation.customer_original_phone = @customer.original_phone
|
||||
|
||||
@@ -2,8 +2,20 @@ import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["select", "phoneField", "birthYearField", "firstNameField", "surnameField", "newCustomerFields"]
|
||||
static values = {
|
||||
existingId: String,
|
||||
existingLabel: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
let initialOptions = [];
|
||||
let initialValue = null;
|
||||
|
||||
if (this.hasExistingIdValue && this.hasExistingLabelValue && this.existingIdValue.length > 0) {
|
||||
initialOptions = [{ id: this.existingIdValue, label: this.existingLabelValue }];
|
||||
initialValue = this.existingIdValue;
|
||||
}
|
||||
|
||||
this.selectInstance = new TomSelect(this.selectTarget, {
|
||||
valueField: 'id',
|
||||
labelField: 'label',
|
||||
@@ -20,10 +32,11 @@ export default class extends Controller {
|
||||
};
|
||||
},
|
||||
|
||||
options: [],
|
||||
options: initialOptions,
|
||||
items: initialValue ? [initialValue] : [],
|
||||
|
||||
load: async (query, callback) => {
|
||||
if (!query.length) return callback();
|
||||
if (!query.length && !initialValue) return callback();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/customers/search?q=${encodeURIComponent(query)}`, {
|
||||
@@ -33,7 +46,11 @@ export default class extends Controller {
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
|
||||
const existingOptionId = this.hasExistingIdValue ? this.existingIdValue : null;
|
||||
const filteredData = data.filter(item => item.id !== existingOptionId);
|
||||
|
||||
callback(filteredData);
|
||||
} catch (error) {
|
||||
console.error('Error loading customers:', error);
|
||||
callback();
|
||||
@@ -53,17 +70,18 @@ export default class extends Controller {
|
||||
}
|
||||
},
|
||||
|
||||
// Events
|
||||
onLoad: (data) => {
|
||||
if (!data || data.length === 0) {
|
||||
if (!this.selectInstance.getValue() && (!data || data.length === 0)) {
|
||||
this.showNewCustomerFields();
|
||||
}
|
||||
},
|
||||
|
||||
onChange: (value) => {
|
||||
if (value === null) {
|
||||
if (value === null || value === '') {
|
||||
this.showNewCustomerFields();
|
||||
}
|
||||
} else if (!value.endsWith('__new')) {
|
||||
this.newCustomerFieldsTarget.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
onItemAdd: (value) => {
|
||||
@@ -76,6 +94,11 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (initialValue) {
|
||||
this.newCustomerFieldsTarget.classList.add('hidden');
|
||||
this.customerSelected(initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
customerSelected(value) {
|
||||
@@ -97,9 +120,15 @@ export default class extends Controller {
|
||||
|
||||
if (customerData && customerData.birthyear) {
|
||||
this.birthYearFieldTarget.value = customerData.birthyear;
|
||||
} else {
|
||||
this.birthYearFieldTarget.value = '';
|
||||
}
|
||||
|
||||
this.newCustomerFieldsTarget.classList.add('hidden');
|
||||
} else {
|
||||
console.warn("Selected customer value format unexpected:", value);
|
||||
this.clearFields();
|
||||
this.showNewCustomerFields();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@ export default class extends Controller {
|
||||
hourStart: 4,
|
||||
hourEnd: 21,
|
||||
},
|
||||
timezone: {
|
||||
zones: [
|
||||
{
|
||||
timezoneName: 'UTC',
|
||||
displayLabel: 'UTC' // Optional: Label for the timezone
|
||||
}
|
||||
]
|
||||
// You might need `primaryTimezone: 'UTC'` here as well,
|
||||
// depending on the exact library version and desired behavior.
|
||||
// Let's start with just zones.
|
||||
},
|
||||
// This is important - set the height to 100%
|
||||
height: '100%',
|
||||
// Make sure it takes full width
|
||||
@@ -52,6 +63,103 @@ export default class extends Controller {
|
||||
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("Error: Could not verify request security token.");
|
||||
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('Reservation deleted.');
|
||||
} else {
|
||||
console.error(`Failed to delete reservation ${reservationId}. Status: ${response.status}`);
|
||||
let errorMessage = 'Error deleting reservation.';
|
||||
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("Error deleting reservation due to a network or script issue.");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -81,6 +189,12 @@ export default class extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get CSRF token from meta tag
|
||||
getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
return token;
|
||||
}
|
||||
|
||||
// Navigation methods - using the global window.calendar
|
||||
prev() {
|
||||
window.calendar.prev();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
||||
|
||||
<%= javascript_importmap_tags %>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css" rel="stylesheet">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "calendar.tailwind", "data-turbo-track": "reload" %>
|
||||
|
||||
|
||||
<%= javascript_importmap_tags %>
|
||||
<style>
|
||||
@@ -59,3 +59,4 @@
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
want
|
||||
@@ -1,4 +1,9 @@
|
||||
<%= form_with(model: reservation, class: "contents", data: { controller: "customer-search" }) do |form| %>
|
||||
<%= form_with(model: reservation, class: "contents",
|
||||
data: {
|
||||
controller: "customer-search",
|
||||
customer_search_existing_id_value: (reservation.persisted? && reservation.customer ? reservation.customer.to_param : nil),
|
||||
customer_search_existing_label_value: (reservation.persisted? && reservation.customer ? "#{reservation.customer.full_name} (#{reservation.customer.original_phone})" : nil)
|
||||
}) do |form| %>
|
||||
<% if reservation.errors.any? %>
|
||||
<div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
|
||||
<h2><%= pluralize(reservation.errors.count, "error") %> prohibited this reservation from being saved:</h2>
|
||||
@@ -13,10 +18,11 @@
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.label :customer %>
|
||||
<%= form.select :customer_id,
|
||||
<%= form.select :customer_composite_key,
|
||||
[], # Start with empty options
|
||||
{ prompt: "Type to search customers..." },
|
||||
{
|
||||
{
|
||||
selected: (reservation.persisted? && reservation.customer ? reservation.customer.to_param : nil),
|
||||
class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full",
|
||||
data: { customer_search_target: "select" }
|
||||
} %>
|
||||
@@ -80,36 +86,3 @@
|
||||
<%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Only set default times for new records (not when editing)
|
||||
<% unless reservation.persisted? %>
|
||||
// Get current date and time
|
||||
const now = new Date();
|
||||
|
||||
// Get local components
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
// Format for datetime-local input (YYYY-MM-DDThh:mm)
|
||||
const localStartTime = `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
document.getElementById('start_time_field').value = localStartTime;
|
||||
|
||||
// Add 30 minutes for end time
|
||||
const endDate = new Date(now.getTime() + 30 * 60000);
|
||||
const endHours = String(endDate.getHours()).padStart(2, '0');
|
||||
const endMinutes = String(endDate.getMinutes()).padStart(2, '0');
|
||||
const localEndTime = `${year}-${month}-${day}T${endHours}:${endMinutes}`;
|
||||
document.getElementById('end_time_field').value = localEndTime;
|
||||
|
||||
// For debugging - add this to see actual values
|
||||
console.log("Start time set to: " + localStartTime);
|
||||
console.log("End time set to: " + localEndTime);
|
||||
console.log("Current browser time: " + now.toString());
|
||||
<% end %>
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
<p class="my-5">
|
||||
<strong class="block font-medium mb-1">Start time:</strong>
|
||||
<%= reservation.start_time %>
|
||||
<%= l reservation.start_time if reservation.start_time %>
|
||||
</p>
|
||||
|
||||
<p class="my-5">
|
||||
<strong class="block font-medium mb-1">End time:</strong>
|
||||
<%= reservation.end_time %>
|
||||
<%= l reservation.end_time if reservation.end_time %>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user