Merge branch '2-napraviti-dodavanje-termina' into 'master'
Added customer composite key Closes #2 See merge request kbr4/zsterminator!2
This commit is contained in:
18
app/javascript/controllers/calendar_controller.js
Normal file
18
app/javascript/controllers/calendar_controller.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.initializeCalendar();
|
||||
}
|
||||
|
||||
initializeCalendar() {
|
||||
// If using a library like FullCalendar
|
||||
const calendar = new FullCalendar.Calendar(this.element, {
|
||||
height: '80vh', // Full viewport height
|
||||
width: '100%',
|
||||
// Other calendar options...
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
}
|
||||
}
|
||||
151
app/javascript/controllers/customer_search_controller.js
Normal file
151
app/javascript/controllers/customer_search_controller.js
Normal file
@@ -0,0 +1,151 @@
|
||||
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',
|
||||
searchField: ['label'],
|
||||
maxItems: 1,
|
||||
create: true,
|
||||
createOnBlur: true,
|
||||
placeholder: 'Type to search customers...',
|
||||
|
||||
create: function(input) {
|
||||
return {
|
||||
id: `${input}__new`,
|
||||
label: `${input} (New Customer)`
|
||||
};
|
||||
},
|
||||
|
||||
options: initialOptions,
|
||||
items: initialValue ? [initialValue] : [],
|
||||
|
||||
load: async (query, callback) => {
|
||||
if (!query.length && !initialValue) return callback();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/customers/search?q=${encodeURIComponent(query)}`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
shouldLoad: function(query) {
|
||||
return query.length >= 2;
|
||||
},
|
||||
|
||||
render: {
|
||||
no_results: (data, escape) => {
|
||||
return '<div class="no-results">No customers found. Fill in the details below.</div>';
|
||||
},
|
||||
option: function(item) {
|
||||
return `<div>${item.label}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
onLoad: (data) => {
|
||||
if (!this.selectInstance.getValue() && (!data || data.length === 0)) {
|
||||
this.showNewCustomerFields();
|
||||
}
|
||||
},
|
||||
|
||||
onChange: (value) => {
|
||||
if (value === null || value === '') {
|
||||
this.showNewCustomerFields();
|
||||
} else if (!value.endsWith('__new')) {
|
||||
this.newCustomerFieldsTarget.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
onItemAdd: (value) => {
|
||||
this.customerSelected(value);
|
||||
},
|
||||
|
||||
onDropdownClose: (dropdown) => {
|
||||
if (!this.selectInstance.getValue()) {
|
||||
this.showNewCustomerFields();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (initialValue) {
|
||||
this.newCustomerFieldsTarget.classList.add('hidden');
|
||||
this.customerSelected(initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
customerSelected(value) {
|
||||
if (value.endsWith('__new')) {
|
||||
const firstName = value.replace('__new', '');
|
||||
this.firstNameFieldTarget.value = firstName;
|
||||
this.showNewCustomerFields();
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstName, surname, phone] = value.split('_');
|
||||
|
||||
if (firstName && surname && phone) {
|
||||
const customerData = this.selectInstance.options[value];
|
||||
|
||||
this.phoneFieldTarget.value = phone;
|
||||
this.firstNameFieldTarget.value = firstName;
|
||||
this.surnameFieldTarget.value = surname;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
showNewCustomerFields() {
|
||||
this.newCustomerFieldsTarget.classList.remove('hidden');
|
||||
}
|
||||
|
||||
clearFields() {
|
||||
this.phoneFieldTarget.value = '';
|
||||
this.firstNameFieldTarget.value = '';
|
||||
this.surnameFieldTarget.value = '';
|
||||
this.birthYearFieldTarget.value = '';
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.selectInstance) {
|
||||
this.selectInstance.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,13 @@ import {Controller} from "@hotwired/stimulus"
|
||||
// Connects to data-controller="main-calendar"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["dateDisplay", "navigation"]
|
||||
|
||||
connect() {
|
||||
// Set height to full viewport
|
||||
document.getElementById('main-calendar').style.height = '100vh';
|
||||
document.getElementById('main-calendar').style.width = '100vw';
|
||||
|
||||
const calendar = new tui.Calendar(document.getElementById('main-calendar'), {
|
||||
defaultView: 'week',
|
||||
usageStatistics: false,
|
||||
@@ -10,12 +16,40 @@ export default class extends Controller {
|
||||
taskView: false,
|
||||
scheduleView: false,
|
||||
eventView: ['time'],
|
||||
startDayOfWeek: 1, // Start week on Monday
|
||||
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
|
||||
width: '100%',
|
||||
template: {
|
||||
timegridDisplayPrimaryTime({time}) {
|
||||
return `${time.getHours()} sati`;
|
||||
},
|
||||
popupDetailLocation(eventObj) {
|
||||
return ''; // Empty location as requested
|
||||
},
|
||||
popupDetailAttendees(eventObj) {
|
||||
return eventObj.attendees[0]; // Show team name
|
||||
},
|
||||
popupDetailState(eventObj) {
|
||||
return '';
|
||||
},
|
||||
popupDetailBody(eventObj) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
calendars: [
|
||||
@@ -25,11 +59,114 @@ export default class extends Controller {
|
||||
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("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();
|
||||
|
||||
calendar.render();
|
||||
|
||||
// Update the date display after rendering
|
||||
this.updateDateDisplay();
|
||||
}
|
||||
|
||||
getCalendardata() {
|
||||
@@ -40,14 +177,81 @@ export default class extends Controller {
|
||||
{
|
||||
id: reservation.id,
|
||||
calendarId: 'cal1',
|
||||
title: reservation.customer.name,
|
||||
title: reservation.customer.first_name + ' ' + reservation.customer.surname + ' (' + reservation.customer.phone + ')',
|
||||
category: 'time',
|
||||
dueDateClass: reservation.dueDateClass,
|
||||
location: reservation.team.name,
|
||||
location: '', // Empty location as requested
|
||||
attendees: [reservation.team.name], // Team name as attendee
|
||||
start: reservation.start_time,
|
||||
end: reservation.end_time
|
||||
}
|
||||
])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
this.updateDateDisplay();
|
||||
}
|
||||
|
||||
next() {
|
||||
window.calendar.next();
|
||||
this.updateDateDisplay();
|
||||
}
|
||||
|
||||
today() {
|
||||
window.calendar.today();
|
||||
this.updateDateDisplay();
|
||||
}
|
||||
|
||||
// Helper for updating the date display
|
||||
updateDateDisplay() {
|
||||
if (this.hasDateDisplayTarget) {
|
||||
const calendar = window.calendar;
|
||||
const currentDate = calendar.getDate();
|
||||
|
||||
// Format the date range based on the current view
|
||||
const view = calendar.getViewName();
|
||||
|
||||
if (view === 'day') {
|
||||
this.dateDisplayTarget.textContent = this.formatDate(currentDate);
|
||||
} else if (view === 'week') {
|
||||
// For week view, show range (e.g., "Aug 1 - 7, 2023")
|
||||
const weekStart = new Date(currentDate);
|
||||
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + (weekStart.getDay() === 0 ? -6 : 1));
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
|
||||
this.dateDisplayTarget.textContent = `${this.formatDateShort(weekStart)} - ${this.formatDateShort(weekEnd)}`;
|
||||
} else if (view === 'month') {
|
||||
// For month view, show month and year (e.g., "August 2023")
|
||||
this.dateDisplayTarget.textContent = currentDate.toLocaleString('default', { month: 'long', year: 'numeric' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for formatting dates
|
||||
formatDate(date) {
|
||||
return date.toLocaleDateString('default', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
formatDateShort(date) {
|
||||
return date.toLocaleDateString('default', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user