+
+
+
+
+
+
-
-
-
+
+
+
Huecos Disponibles
+
+
+ No hay huecos libres para esta duración.
+
-
-
-
Huecos Disponibles
-
-
- No hay huecos libres para esta duración.
+
+
+
+
+ Ya tienes otra cita que se cruza con esas fechas.
@@ -689,6 +714,10 @@ function renderServices(servicesToRender = localServices) {
document.getElementById('durationInput').value = "60";
pickerSelectedDate = "";
pickerSelectedTime = "";
+ document.getElementById('multiDayToggle').checked = false;
+ document.getElementById('mdStartDate').value = "";
+ document.getElementById('mdEndDate').value = "";
+ toggleMultiDay(false);
buildDayCarousel();
checkSaveButton();
const modal = document.getElementById('actionModal');
@@ -792,7 +821,18 @@ function renderServices(servicesToRender = localServices) {
}
function checkSaveButton() {
const btn = document.getElementById('btnSaveAppt');
- if (pickerSelectedDate && pickerSelectedTime) {
+ const isMultiDay = document.getElementById('multiDayToggle').checked;
+ let ok;
+ if (isMultiDay) {
+ const range = getMultiDayRange();
+ const hasValidRange = !!range && range.end > range.start;
+ const overlaps = hasValidRange && getMultiDayOverlap(range.start, range.end);
+ document.getElementById('mdOverlapMsg').classList.toggle('hidden', !overlaps);
+ ok = hasValidRange && !overlaps;
+ } else {
+ ok = !!(pickerSelectedDate && pickerSelectedTime);
+ }
+ if (ok) {
btn.disabled = false;
btn.classList.remove('bg-slate-300', 'cursor-not-allowed');
btn.classList.add('bg-primary-dynamic', 'active:scale-95', 'shadow-lg');
@@ -802,6 +842,42 @@ function renderServices(servicesToRender = localServices) {
btn.classList.remove('bg-primary-dynamic', 'active:scale-95', 'shadow-lg');
}
}
+ // 🏗️ Obras/instalaciones de varios días: en vez de elegir un hueco dentro de un
+ // mismo día, el operario indica fecha+hora exacta de inicio y de fin (pueden ser
+ // días distintos). Internamente seguimos usando el mismo modelo de siempre
+ // (scheduled_date + scheduled_time + duration_minutes): la duración se calcula
+ // como los minutos reales entre el inicio y el fin.
+ function toggleMultiDay(checked) {
+ document.getElementById('singleDayFields').classList.toggle('hidden', checked);
+ document.getElementById('multiDayFields').classList.toggle('hidden', !checked);
+ checkSaveButton();
+ }
+ function getMultiDayRange() {
+ const sd = document.getElementById('mdStartDate').value;
+ const st = document.getElementById('mdStartTime').value || "08:00";
+ const ed = document.getElementById('mdEndDate').value;
+ const et = document.getElementById('mdEndTime').value || "18:00";
+ if (!sd || !ed) return null;
+ const start = new Date(`${sd}T${st}`);
+ const end = new Date(`${ed}T${et}`);
+ if (isNaN(start) || isNaN(end)) return null;
+ return { start, end };
+ }
+ // Comprueba solapes contra TODAS las citas ya agendadas del operario, sin importar
+ // en qué día caigan (a diferencia de getOccupiedRanges, que solo mira un día suelto).
+ function getMultiDayOverlap(start, end) {
+ if (!start || !end) return false;
+ const currentId = Number(document.getElementById('detId').value);
+ return globalAllServices.some(s => {
+ if (s.id === currentId) return false;
+ const raw = s.raw_data || {};
+ if (!raw.scheduled_date || !raw.scheduled_time) return false;
+ const exStart = new Date(`${raw.scheduled_date}T${raw.scheduled_time}`);
+ const exDur = parseInt(raw.duration_minutes || 60);
+ const exEnd = new Date(exStart.getTime() + exDur * 60000);
+ return start < exEnd && end > exStart;
+ });
+ }
function getOccupiedRanges(dateStr) {
let ranges = [];
globalAllServices.forEach(s => {
@@ -903,8 +979,23 @@ function renderServices(servicesToRender = localServices) {
}
async function saveAppointment() {
const id = document.getElementById('detId').value;
- const duration = document.getElementById('durationInput').value;
- if (!pickerSelectedDate || !pickerSelectedTime) return alert("Selecciona un hueco disponible.");
+ const isMultiDay = document.getElementById('multiDayToggle').checked;
+
+ let appointmentDate, appointmentTime, duration;
+ if (isMultiDay) {
+ const range = getMultiDayRange();
+ if (!range || range.end <= range.start) return alert("Revisa las fechas/horas de inicio y fin de la obra.");
+ if (getMultiDayOverlap(range.start, range.end)) return alert("Esas fechas se cruzan con otra cita ya agendada.");
+ appointmentDate = document.getElementById('mdStartDate').value;
+ appointmentTime = document.getElementById('mdStartTime').value || "08:00";
+ duration = Math.round((range.end - range.start) / 60000);
+ } else {
+ if (!pickerSelectedDate || !pickerSelectedTime) return alert("Selecciona un hueco disponible.");
+ appointmentDate = pickerSelectedDate;
+ appointmentTime = pickerSelectedTime;
+ duration = document.getElementById('durationInput').value;
+ }
+
const btn = document.getElementById('btnSaveAppt');
const originalContent = btn.innerHTML;
btn.innerHTML = `
Agendando...`;
@@ -916,8 +1007,8 @@ function renderServices(servicesToRender = localServices) {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({
- date: pickerSelectedDate,
- time: pickerSelectedTime,
+ date: appointmentDate,
+ time: appointmentTime,
duration_minutes: duration,
status_operativo: statusToSave
})
diff --git a/calendario.html b/calendario.html
index 5407def..e8e881c 100644
--- a/calendario.html
+++ b/calendario.html
@@ -234,7 +234,11 @@
+
+
+
Cambio de Estado
@@ -437,7 +448,11 @@
-
+
+
+
@@ -1044,8 +1066,23 @@
document.getElementById('dateInput').value = raw.scheduled_date || "";
document.getElementById('timeInput').value = raw.scheduled_time || "";
- document.getElementById('durationInput').value = raw.duration_minutes || "60";
-
+ const existingDuration = parseInt(raw.duration_minutes || 60);
+ // 🏗️ Si la duración guardada no cabe en el mismo día (p.ej. una obra de varios
+ // días), abrimos el modal ya en modo "varios días" con la fecha/hora de fin
+ // calculada, en vez de forzar el desplegable de horas (que solo llega a 4h).
+ if (raw.scheduled_date && raw.scheduled_time && existingDuration > 240) {
+ const startDT = new Date(`${raw.scheduled_date}T${raw.scheduled_time}`);
+ const endDT = new Date(startDT.getTime() + existingDuration * 60000);
+ document.getElementById('multiDayToggle').checked = true;
+ document.getElementById('endDateInput').value = toISODate(endDT);
+ document.getElementById('endTimeInput').value = `${String(endDT.getHours()).padStart(2,'0')}:${String(endDT.getMinutes()).padStart(2,'0')}`;
+ document.getElementById('durationInput').value = "60";
+ } else {
+ document.getElementById('multiDayToggle').checked = false;
+ document.getElementById('durationInput').value = existingDuration || "60";
+ }
+ toggleMultiDay(document.getElementById('multiDayToggle').checked);
+
const dbStat = raw.status_operativo;
if(dbStat) document.getElementById('detStatusMap').value = dbStat;
@@ -1851,8 +1888,11 @@
document.getElementById('nsAddress').value = "";
document.getElementById('nsDesc').value = "";
document.getElementById('nsTime').value = "";
- document.getElementById('nsDate').value = selectedDateStr;
-
+ document.getElementById('nsDate').value = selectedDateStr;
+ document.getElementById('multiDayToggleNew').checked = false;
+ document.getElementById('endDateInputNew').value = "";
+ toggleMultiDayNew(false);
+
renderNewServiceAgenda(); // <-- Cargamos la ruta del día
const modal = document.getElementById('newServiceModal');
@@ -1943,11 +1983,37 @@
} catch (e) {}
}
+ // 🏗️ Obras/instalaciones de varios días: en vez de una duración en minutos que
+ // encaja dentro del mismo día, dejamos indicar fecha+hora de fin (puede ser un día
+ // distinto). Seguimos usando el mismo modelo de siempre (scheduled_date +
+ // scheduled_time + duration_minutes): la duración se calcula como los minutos
+ // reales entre el inicio y el fin.
+ function toggleMultiDay(checked) {
+ document.getElementById('singleDayDuration').classList.toggle('hidden', checked);
+ document.getElementById('multiDayFields').classList.toggle('hidden', !checked);
+ }
+ function getAppointmentDuration() {
+ if (!document.getElementById('multiDayToggle').checked) {
+ return parseInt(document.getElementById('durationInput').value) || 60;
+ }
+ const date = document.getElementById('dateInput').value;
+ const time = document.getElementById('timeInput').value;
+ const endDate = document.getElementById('endDateInput').value;
+ const endTime = document.getElementById('endTimeInput').value || "18:00";
+ if (!date || !time || !endDate) return null;
+ const start = new Date(`${date}T${time}`);
+ const end = new Date(`${endDate}T${endTime}`);
+ if (isNaN(start) || isNaN(end) || end <= start) return null;
+ return Math.round((end - start) / 60000);
+ }
+
async function saveAppointment() {
const id = document.getElementById('detId').value;
const date = document.getElementById('dateInput').value;
const time = document.getElementById('timeInput').value;
- const duration = parseInt(document.getElementById('durationInput').value) || 60;
+ const isMultiDay = document.getElementById('multiDayToggle').checked;
+ const duration = getAppointmentDuration();
+ if (isMultiDay && duration === null) return alert("Revisa las fechas/horas de inicio y fin de la obra.");
let statusMap = document.getElementById('detStatusMap').value;
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
@@ -1957,20 +2023,18 @@
// 🛑 RADAR ANTI-SOLAPAMIENTO (PERMISIVO)
if (date && time) {
- let [newH, newM] = time.split(':').map(Number);
- let newStartMin = newH * 60 + newM;
- let newEndMin = newStartMin + duration;
+ const newStart = new Date(`${date}T${time}`);
+ const newEnd = new Date(newStart.getTime() + duration * 60000);
const solapamiento = localServices.find(s => {
if (String(s.id) === String(id)) return false;
const sDate = String(s.raw_data.scheduled_date || "").trim();
const sTime = String(s.raw_data.scheduled_time || "").trim();
- if (sDate === date && sTime && sTime.includes(':')) {
- let [sH, sM] = sTime.split(':').map(Number);
- let sDur = parseInt(s.raw_data.duration_minutes || 60);
- let sStartMin = sH * 60 + sM;
- let sEndMin = sStartMin + sDur;
- return (newStartMin < sEndMin && newEndMin > sStartMin);
+ if (sDate && sTime && sTime.includes(':')) {
+ const sStart = new Date(`${sDate}T${sTime}`);
+ const sDur = parseInt(s.raw_data.duration_minutes || 60);
+ const sEnd = new Date(sStart.getTime() + sDur * 60000);
+ return (newStart < sEnd && newEnd > sStart);
}
return false;
});
@@ -2004,28 +2068,43 @@
finally { btn.innerHTML = originalContent; btn.disabled = false; safeLoadIcons(); }
}
+ function toggleMultiDayNew(checked) {
+ document.getElementById('singleDayDurationNew').classList.toggle('hidden', checked);
+ document.getElementById('multiDayFieldsNew').classList.toggle('hidden', !checked);
+ }
+
async function saveNewServiceApp(e) {
e.preventDefault();
-
+
const date = document.getElementById('nsDate').value;
const time = document.getElementById('nsTime').value;
- const duration = parseInt(document.getElementById('nsDuration').value) || 60;
+ let duration;
+ if (document.getElementById('multiDayToggleNew').checked) {
+ const endDate = document.getElementById('endDateInputNew').value;
+ const endTime = document.getElementById('endTimeInputNew').value || "18:00";
+ if (!date || !time || !endDate) { alert("Revisa las fechas/horas de inicio y fin de la obra."); return; }
+ const start = new Date(`${date}T${time}`);
+ const end = new Date(`${endDate}T${endTime}`);
+ if (isNaN(start) || isNaN(end) || end <= start) { alert("Revisa las fechas/horas de inicio y fin de la obra."); return; }
+ duration = Math.round((end - start) / 60000);
+ } else {
+ duration = parseInt(document.getElementById('nsDuration').value) || 60;
+ }
- // 🛑 RADAR ANTI-SOLAPAMIENTO EN CREACIÓN (PERMISIVO)
+ // 🛑 RADAR ANTI-SOLAPAMIENTO EN CREACIÓN (PERMISIVO) — comparamos por fecha/hora
+ // completas para detectar cruces aunque la obra ocupe varios días.
if (date && time) {
- let [newH, newM] = time.split(':').map(Number);
- let newStartMin = newH * 60 + newM;
- let newEndMin = newStartMin + duration;
+ const newStart = new Date(`${date}T${time}`);
+ const newEnd = new Date(newStart.getTime() + duration * 60000);
const solapamiento = localServices.find(s => {
const sDate = String(s.raw_data.scheduled_date || "").trim();
const sTime = String(s.raw_data.scheduled_time || "").trim();
- if (sDate === date && sTime && sTime.includes(':')) {
- let [sH, sM] = sTime.split(':').map(Number);
- let sDur = parseInt(s.raw_data.duration_minutes || 60);
- let sStartMin = sH * 60 + sM;
- let sEndMin = sStartMin + sDur;
- return (newStartMin < sEndMin && newEndMin > sStartMin);
+ if (sDate && sTime && sTime.includes(':')) {
+ const sStart = new Date(`${sDate}T${sTime}`);
+ const sDur = parseInt(s.raw_data.duration_minutes || 60);
+ const sEnd = new Date(sStart.getTime() + sDur * 60000);
+ return (newStart < sEnd && newEnd > sStart);
}
return false;
});