Añade modo "obra de varios días" en agendar/reagendar/nuevo servicio, sin tope de 4h
This commit is contained in:
149
asignados.html
149
asignados.html
@@ -133,33 +133,58 @@
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<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 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>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<select id="durationInput" onchange="renderTimeSlots()" class="w-full bg-slate-50 border border-slate-200 p-4 rounded-xl text-sm font-black text-slate-700 shadow-inner outline-none focus:ring-2 focus:ring-amber-500 transition-all appearance-none pr-10 cursor-pointer">
|
||||
<option value="15">15 Minutos (Muy rápido)</option>
|
||||
<option value="30">30 Minutos</option>
|
||||
<option value="45">45 Minutos</option>
|
||||
<option value="60" selected>1 Hora (Estándar)</option>
|
||||
<option value="90">1 Hora y Media</option>
|
||||
<option value="120">2 Horas</option>
|
||||
<option value="180">3 Horas</option>
|
||||
<option value="240">4 Horas (Media jornada)</option>
|
||||
</select>
|
||||
<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 class="relative">
|
||||
<select id="durationInput" onchange="renderTimeSlots()" class="w-full bg-slate-50 border border-slate-200 p-4 rounded-xl text-sm font-black text-slate-700 shadow-inner outline-none focus:ring-2 focus:ring-amber-500 transition-all appearance-none pr-10 cursor-pointer">
|
||||
<option value="15">15 Minutos (Muy rápido)</option>
|
||||
<option value="30">30 Minutos</option>
|
||||
<option value="45">45 Minutos</option>
|
||||
<option value="60" selected>1 Hora (Estándar)</option>
|
||||
<option value="90">1 Hora y Media</option>
|
||||
<option value="120">2 Horas</option>
|
||||
<option value="180">3 Horas</option>
|
||||
<option value="240">4 Horas (Media jornada)</option>
|
||||
</select>
|
||||
<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>
|
||||
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-2 flex items-center gap-1.5"><i data-lucide="calendar" class="w-4 h-4"></i> Seleccionar Día</p>
|
||||
<div id="dayCarousel" class="flex overflow-x-auto gap-2 pb-2 no-scrollbar"></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="watch" class="w-4 h-4"></i> Huecos Disponibles</p>
|
||||
<div id="timeGrid" class="grid grid-cols-4 gap-2"></div>
|
||||
<p id="noSlotsMsg" class="hidden text-xs text-rose-500 font-bold bg-rose-50 p-3 rounded-xl border border-rose-100 text-center mt-2">
|
||||
No hay huecos libres para esta duración.
|
||||
</p>
|
||||
</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" class="w-4 h-4"></i> Seleccionar Día</p>
|
||||
<div id="dayCarousel" class="flex overflow-x-auto gap-2 pb-2 no-scrollbar"></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="watch" class="w-4 h-4"></i> Huecos Disponibles</p>
|
||||
<div id="timeGrid" class="grid grid-cols-4 gap-2"></div>
|
||||
<p id="noSlotsMsg" class="hidden text-xs text-rose-500 font-bold bg-rose-50 p-3 rounded-xl border border-rose-100 text-center mt-2">
|
||||
No hay huecos libres para esta duración.
|
||||
<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>
|
||||
@@ -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 = `<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> 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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user