Añade modo "obra de varios días" en agendar/reagendar/nuevo servicio, sin tope de 4h
This commit is contained in:
@@ -133,6 +133,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr class="border-slate-100 mb-6">
|
<hr class="border-slate-100 mb-6">
|
||||||
<div class="bg-white border border-slate-200 rounded-[1.5rem] p-5 shadow-sm space-y-5">
|
<div class="bg-white border border-slate-200 rounded-[1.5rem] p-5 shadow-sm space-y-5">
|
||||||
|
<label class="flex items-center gap-2 bg-amber-50 border border-amber-100 rounded-xl px-3 py-2.5 cursor-pointer">
|
||||||
|
<input type="checkbox" id="multiDayToggle" onchange="toggleMultiDay(this.checked)" class="w-4 h-4 rounded text-amber-600">
|
||||||
|
<span class="text-[10px] font-black text-amber-700 uppercase tracking-widest">Obra / instalación de varios días</span>
|
||||||
|
</label>
|
||||||
|
<div id="singleDayFields" class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest flex items-center gap-1.5"><i data-lucide="clock" class="w-4 h-4"></i> Duración Estimada</p>
|
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest flex items-center gap-1.5"><i data-lucide="clock" class="w-4 h-4"></i> Duración Estimada</p>
|
||||||
@@ -163,6 +168,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="multiDayFields" class="hidden space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-2 flex items-center gap-1.5"><i data-lucide="calendar-clock" class="w-4 h-4"></i> Inicio</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="date" id="mdStartDate" onchange="checkSaveButton()" class="flex-1 bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-inner outline-none">
|
||||||
|
<input type="time" id="mdStartTime" value="08:00" onchange="checkSaveButton()" class="w-28 bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-inner outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-2 flex items-center gap-1.5"><i data-lucide="calendar-clock" class="w-4 h-4"></i> Fin</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="date" id="mdEndDate" onchange="checkSaveButton()" class="flex-1 bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-inner outline-none">
|
||||||
|
<input type="time" id="mdEndTime" value="18:00" onchange="checkSaveButton()" class="w-28 bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-inner outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="mdOverlapMsg" class="hidden text-xs text-rose-500 font-bold bg-rose-50 p-3 rounded-xl border border-rose-100 text-center">
|
||||||
|
Ya tienes otra cita que se cruza con esas fechas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-4 shrink-0 bg-white border-t border-slate-100">
|
<div class="pt-4 shrink-0 bg-white border-t border-slate-100">
|
||||||
<button onclick="saveAppointment()" id="btnSaveAppt" disabled class="w-full bg-slate-300 text-white font-black py-4 rounded-2xl shadow-sm transition-all flex items-center justify-center gap-2 uppercase tracking-widest text-xs cursor-not-allowed">
|
<button onclick="saveAppointment()" id="btnSaveAppt" disabled class="w-full bg-slate-300 text-white font-black py-4 rounded-2xl shadow-sm transition-all flex items-center justify-center gap-2 uppercase tracking-widest text-xs cursor-not-allowed">
|
||||||
@@ -689,6 +714,10 @@ function renderServices(servicesToRender = localServices) {
|
|||||||
document.getElementById('durationInput').value = "60";
|
document.getElementById('durationInput').value = "60";
|
||||||
pickerSelectedDate = "";
|
pickerSelectedDate = "";
|
||||||
pickerSelectedTime = "";
|
pickerSelectedTime = "";
|
||||||
|
document.getElementById('multiDayToggle').checked = false;
|
||||||
|
document.getElementById('mdStartDate').value = "";
|
||||||
|
document.getElementById('mdEndDate').value = "";
|
||||||
|
toggleMultiDay(false);
|
||||||
buildDayCarousel();
|
buildDayCarousel();
|
||||||
checkSaveButton();
|
checkSaveButton();
|
||||||
const modal = document.getElementById('actionModal');
|
const modal = document.getElementById('actionModal');
|
||||||
@@ -792,7 +821,18 @@ function renderServices(servicesToRender = localServices) {
|
|||||||
}
|
}
|
||||||
function checkSaveButton() {
|
function checkSaveButton() {
|
||||||
const btn = document.getElementById('btnSaveAppt');
|
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.disabled = false;
|
||||||
btn.classList.remove('bg-slate-300', 'cursor-not-allowed');
|
btn.classList.remove('bg-slate-300', 'cursor-not-allowed');
|
||||||
btn.classList.add('bg-primary-dynamic', 'active:scale-95', 'shadow-lg');
|
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');
|
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) {
|
function getOccupiedRanges(dateStr) {
|
||||||
let ranges = [];
|
let ranges = [];
|
||||||
globalAllServices.forEach(s => {
|
globalAllServices.forEach(s => {
|
||||||
@@ -903,8 +979,23 @@ function renderServices(servicesToRender = localServices) {
|
|||||||
}
|
}
|
||||||
async function saveAppointment() {
|
async function saveAppointment() {
|
||||||
const id = document.getElementById('detId').value;
|
const id = document.getElementById('detId').value;
|
||||||
const duration = document.getElementById('durationInput').value;
|
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.");
|
if (!pickerSelectedDate || !pickerSelectedTime) return alert("Selecciona un hueco disponible.");
|
||||||
|
appointmentDate = pickerSelectedDate;
|
||||||
|
appointmentTime = pickerSelectedTime;
|
||||||
|
duration = document.getElementById('durationInput').value;
|
||||||
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('btnSaveAppt');
|
const btn = document.getElementById('btnSaveAppt');
|
||||||
const originalContent = btn.innerHTML;
|
const originalContent = btn.innerHTML;
|
||||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Agendando...`;
|
btn.innerHTML = `<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Agendando...`;
|
||||||
@@ -916,8 +1007,8 @@ function renderServices(servicesToRender = localServices) {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
date: pickerSelectedDate,
|
date: appointmentDate,
|
||||||
time: pickerSelectedTime,
|
time: appointmentTime,
|
||||||
duration_minutes: duration,
|
duration_minutes: duration,
|
||||||
status_operativo: statusToSave
|
status_operativo: statusToSave
|
||||||
})
|
})
|
||||||
|
|||||||
129
calendario.html
129
calendario.html
@@ -234,7 +234,11 @@
|
|||||||
<div id="modalAgendaList" class="flex flex-wrap gap-2"></div>
|
<div id="modalAgendaList" class="flex flex-wrap gap-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<label class="flex items-center gap-2 bg-amber-50 border border-amber-100 rounded-xl px-3 py-2.5 cursor-pointer">
|
||||||
|
<input type="checkbox" id="multiDayToggle" onchange="toggleMultiDay(this.checked)" class="w-4 h-4 rounded text-amber-600">
|
||||||
|
<span class="text-[10px] font-black text-amber-700 uppercase tracking-widest">Obra / instalación de varios días</span>
|
||||||
|
</label>
|
||||||
|
<div id="singleDayDuration">
|
||||||
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1 ml-1 text-left">Duración Estimada</label>
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1 ml-1 text-left">Duración Estimada</label>
|
||||||
<div class="relative text-left">
|
<div class="relative text-left">
|
||||||
<select id="durationInput" class="w-full bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-sm outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all appearance-none pr-8 cursor-pointer">
|
<select id="durationInput" class="w-full bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-sm outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all appearance-none pr-8 cursor-pointer">
|
||||||
@@ -249,11 +253,18 @@
|
|||||||
<option value="150">2 h 30 min</option>
|
<option value="150">2 h 30 min</option>
|
||||||
<option value="180">3 h</option>
|
<option value="180">3 h</option>
|
||||||
<option value="210">3 h 30 min</option>
|
<option value="210">3 h 30 min</option>
|
||||||
<option value="240">4 h (Máx)</option>
|
<option value="240">4 h</option>
|
||||||
</select>
|
</select>
|
||||||
<i data-lucide="clock" class="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
|
<i data-lucide="clock" class="w-4 h-4 absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="multiDayFields" class="hidden">
|
||||||
|
<label class="block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1 ml-1 text-left">Fin de la obra</label>
|
||||||
|
<div class="grid grid-cols-2 gap-3 text-left">
|
||||||
|
<input type="date" id="endDateInput" class="w-full bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-sm outline-none">
|
||||||
|
<input type="time" id="endTimeInput" value="18:00" class="w-full bg-slate-50 border border-slate-200 p-3 rounded-xl text-xs font-bold text-slate-700 shadow-sm outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-2 text-left">
|
<div class="pt-2 text-left">
|
||||||
<p class="text-[10px] font-black text-slate-800 uppercase ml-1 flex items-center gap-1.5 mb-1.5"><i data-lucide="arrow-right-left" class="w-4 h-4 text-blue-500"></i> Cambio de Estado</p>
|
<p class="text-[10px] font-black text-slate-800 uppercase ml-1 flex items-center gap-1.5 mb-1.5"><i data-lucide="arrow-right-left" class="w-4 h-4 text-blue-500"></i> Cambio de Estado</p>
|
||||||
@@ -437,7 +448,11 @@
|
|||||||
<div id="nsAgendaList" class="flex flex-col gap-2 w-full"></div>
|
<div id="nsAgendaList" class="flex flex-col gap-2 w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full">
|
<label class="w-full flex items-center gap-2 bg-amber-50 border border-amber-100 rounded-xl px-3 py-2.5 cursor-pointer">
|
||||||
|
<input type="checkbox" id="multiDayToggleNew" onchange="toggleMultiDayNew(this.checked)" class="w-4 h-4 rounded text-amber-600">
|
||||||
|
<span class="text-[10px] font-black text-amber-700 uppercase tracking-widest">Obra / instalación de varios días</span>
|
||||||
|
</label>
|
||||||
|
<div class="w-full" id="singleDayDurationNew">
|
||||||
<label class="label-modern">Tiempo Estimado</label>
|
<label class="label-modern">Tiempo Estimado</label>
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<select id="nsDuration" class="input-modern w-full appearance-none pr-8 cursor-pointer">
|
<select id="nsDuration" class="input-modern w-full appearance-none pr-8 cursor-pointer">
|
||||||
@@ -453,6 +468,13 @@
|
|||||||
<i data-lucide="chevron-down" class="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
|
<i data-lucide="chevron-down" class="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full hidden" id="multiDayFieldsNew">
|
||||||
|
<label class="label-modern">Fin de la obra</label>
|
||||||
|
<div class="grid grid-cols-2 gap-3 w-full">
|
||||||
|
<input type="date" id="endDateInputNew" class="input-modern w-full">
|
||||||
|
<input type="time" id="endTimeInputNew" value="18:00" class="input-modern w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<label class="label-modern">Descripción del Trabajo</label>
|
<label class="label-modern">Descripción del Trabajo</label>
|
||||||
@@ -1044,7 +1066,22 @@
|
|||||||
|
|
||||||
document.getElementById('dateInput').value = raw.scheduled_date || "";
|
document.getElementById('dateInput').value = raw.scheduled_date || "";
|
||||||
document.getElementById('timeInput').value = raw.scheduled_time || "";
|
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;
|
const dbStat = raw.status_operativo;
|
||||||
if(dbStat) document.getElementById('detStatusMap').value = dbStat;
|
if(dbStat) document.getElementById('detStatusMap').value = dbStat;
|
||||||
@@ -1852,6 +1889,9 @@
|
|||||||
document.getElementById('nsDesc').value = "";
|
document.getElementById('nsDesc').value = "";
|
||||||
document.getElementById('nsTime').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
|
renderNewServiceAgenda(); // <-- Cargamos la ruta del día
|
||||||
|
|
||||||
@@ -1943,11 +1983,37 @@
|
|||||||
} catch (e) {}
|
} 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() {
|
async function saveAppointment() {
|
||||||
const id = document.getElementById('detId').value;
|
const id = document.getElementById('detId').value;
|
||||||
const date = document.getElementById('dateInput').value;
|
const date = document.getElementById('dateInput').value;
|
||||||
const time = document.getElementById('timeInput').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;
|
let statusMap = document.getElementById('detStatusMap').value;
|
||||||
|
|
||||||
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
|
const selectedSt = systemStatuses.find(st => String(st.id) === String(statusMap));
|
||||||
@@ -1957,20 +2023,18 @@
|
|||||||
|
|
||||||
// 🛑 RADAR ANTI-SOLAPAMIENTO (PERMISIVO)
|
// 🛑 RADAR ANTI-SOLAPAMIENTO (PERMISIVO)
|
||||||
if (date && time) {
|
if (date && time) {
|
||||||
let [newH, newM] = time.split(':').map(Number);
|
const newStart = new Date(`${date}T${time}`);
|
||||||
let newStartMin = newH * 60 + newM;
|
const newEnd = new Date(newStart.getTime() + duration * 60000);
|
||||||
let newEndMin = newStartMin + duration;
|
|
||||||
|
|
||||||
const solapamiento = localServices.find(s => {
|
const solapamiento = localServices.find(s => {
|
||||||
if (String(s.id) === String(id)) return false;
|
if (String(s.id) === String(id)) return false;
|
||||||
const sDate = String(s.raw_data.scheduled_date || "").trim();
|
const sDate = String(s.raw_data.scheduled_date || "").trim();
|
||||||
const sTime = String(s.raw_data.scheduled_time || "").trim();
|
const sTime = String(s.raw_data.scheduled_time || "").trim();
|
||||||
if (sDate === date && sTime && sTime.includes(':')) {
|
if (sDate && sTime && sTime.includes(':')) {
|
||||||
let [sH, sM] = sTime.split(':').map(Number);
|
const sStart = new Date(`${sDate}T${sTime}`);
|
||||||
let sDur = parseInt(s.raw_data.duration_minutes || 60);
|
const sDur = parseInt(s.raw_data.duration_minutes || 60);
|
||||||
let sStartMin = sH * 60 + sM;
|
const sEnd = new Date(sStart.getTime() + sDur * 60000);
|
||||||
let sEndMin = sStartMin + sDur;
|
return (newStart < sEnd && newEnd > sStart);
|
||||||
return (newStartMin < sEndMin && newEndMin > sStartMin);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -2004,28 +2068,43 @@
|
|||||||
finally { btn.innerHTML = originalContent; btn.disabled = false; safeLoadIcons(); }
|
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) {
|
async function saveNewServiceApp(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const date = document.getElementById('nsDate').value;
|
const date = document.getElementById('nsDate').value;
|
||||||
const time = document.getElementById('nsTime').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) {
|
if (date && time) {
|
||||||
let [newH, newM] = time.split(':').map(Number);
|
const newStart = new Date(`${date}T${time}`);
|
||||||
let newStartMin = newH * 60 + newM;
|
const newEnd = new Date(newStart.getTime() + duration * 60000);
|
||||||
let newEndMin = newStartMin + duration;
|
|
||||||
|
|
||||||
const solapamiento = localServices.find(s => {
|
const solapamiento = localServices.find(s => {
|
||||||
const sDate = String(s.raw_data.scheduled_date || "").trim();
|
const sDate = String(s.raw_data.scheduled_date || "").trim();
|
||||||
const sTime = String(s.raw_data.scheduled_time || "").trim();
|
const sTime = String(s.raw_data.scheduled_time || "").trim();
|
||||||
if (sDate === date && sTime && sTime.includes(':')) {
|
if (sDate && sTime && sTime.includes(':')) {
|
||||||
let [sH, sM] = sTime.split(':').map(Number);
|
const sStart = new Date(`${sDate}T${sTime}`);
|
||||||
let sDur = parseInt(s.raw_data.duration_minutes || 60);
|
const sDur = parseInt(s.raw_data.duration_minutes || 60);
|
||||||
let sStartMin = sH * 60 + sM;
|
const sEnd = new Date(sStart.getTime() + sDur * 60000);
|
||||||
let sEndMin = sStartMin + sDur;
|
return (newStart < sEnd && newEnd > sStart);
|
||||||
return (newStartMin < sEndMin && newEndMin > sStartMin);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user