diff --git a/asignados.html b/asignados.html index ab30936..0feffbf 100644 --- a/asignados.html +++ b/asignados.html @@ -133,33 +133,58 @@
-
-
-

Duración Estimada

+ +
+
+
+

Duración Estimada

+
+
+ + +
-
- - +
+

Seleccionar Día

+
+
+
+

Huecos Disponibles

+
+
-
-

Seleccionar Día

-
-
-
-

Huecos Disponibles

-
-
@@ -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; });