783 lines
29 KiB
JavaScript
783 lines
29 KiB
JavaScript
/**
|
|
* My Presence Page
|
|
* Personal calendar for marking daily presence
|
|
*/
|
|
|
|
let currentUser = null;
|
|
let currentDate = new Date();
|
|
let presenceData = {};
|
|
let parkingData = {};
|
|
let currentAssignmentId = null;
|
|
let weeklyClosingDays = [];
|
|
let specificClosingDays = [];
|
|
let statusDate = new Date();
|
|
let statusViewMode = 'daily';
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
currentUser = await api.requireAuth();
|
|
if (!currentUser) return;
|
|
|
|
await Promise.all([loadPresences(), loadParkingAssignments(), loadClosingDays()]);
|
|
|
|
// Initialize Modal Logic
|
|
ModalLogic.init({
|
|
onMarkPresence: handleMarkPresence,
|
|
onClearPresence: handleClearPresence,
|
|
onReleaseParking: handleReleaseParking,
|
|
onReassignParking: handleReassignParking
|
|
});
|
|
|
|
renderCalendar();
|
|
setupEventListeners();
|
|
|
|
// Initialize Parking Status
|
|
initParkingStatus();
|
|
setupStatusListeners();
|
|
|
|
// Initialize Exclusion Logic
|
|
initExclusionLogic();
|
|
});
|
|
|
|
async function loadPresences() {
|
|
const year = currentDate.getFullYear();
|
|
const month = currentDate.getMonth();
|
|
const firstDay = new Date(year, month, 1);
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
const startDate = utils.formatDate(firstDay);
|
|
const endDate = utils.formatDate(lastDay);
|
|
|
|
const response = await api.get(`/api/presence/my-presences?start_date=${startDate}&end_date=${endDate}`);
|
|
if (response && response.ok) {
|
|
const presences = await response.json();
|
|
presenceData = {};
|
|
presences.forEach(p => {
|
|
presenceData[p.date] = p;
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadParkingAssignments() {
|
|
const year = currentDate.getFullYear();
|
|
const month = currentDate.getMonth();
|
|
const firstDay = new Date(year, month, 1);
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
const startDate = utils.formatDate(firstDay);
|
|
const endDate = utils.formatDate(lastDay);
|
|
|
|
const response = await api.get(`/api/parking/my-assignments?start_date=${startDate}&end_date=${endDate}`);
|
|
if (response && response.ok) {
|
|
const assignments = await response.json();
|
|
parkingData = {};
|
|
assignments.forEach(a => {
|
|
parkingData[a.date] = a;
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
async function loadClosingDays() {
|
|
if (!currentUser.office_id) return;
|
|
try {
|
|
const [weeklyRes, specificRes] = await Promise.all([
|
|
api.get(`/api/offices/${currentUser.office_id}/weekly-closing-days`),
|
|
api.get(`/api/offices/${currentUser.office_id}/closing-days`)
|
|
]);
|
|
|
|
if (weeklyRes && weeklyRes.ok) {
|
|
const days = await weeklyRes.json();
|
|
weeklyClosingDays = days.map(d => d.weekday);
|
|
}
|
|
|
|
if (specificRes && specificRes.ok) {
|
|
specificClosingDays = await specificRes.json();
|
|
}
|
|
} catch (e) {
|
|
console.error('Error loading closing days:', e);
|
|
}
|
|
}
|
|
|
|
function renderCalendar() {
|
|
const year = currentDate.getFullYear();
|
|
const month = currentDate.getMonth();
|
|
const weekStartDay = currentUser.week_start_day || 1; // 0=Sunday, 1=Monday (default to Monday)
|
|
|
|
// Update month header
|
|
document.getElementById('currentMonth').textContent = `${utils.getMonthName(month)} ${year}`;
|
|
|
|
// Get calendar info
|
|
const daysInMonth = utils.getDaysInMonth(year, month);
|
|
const firstDayOfMonth = new Date(year, month, 1).getDay(); // 0=Sunday
|
|
const today = new Date();
|
|
|
|
// Calculate offset based on week start preference
|
|
let firstDayOffset = firstDayOfMonth - weekStartDay;
|
|
if (firstDayOffset < 0) firstDayOffset += 7;
|
|
|
|
// Build calendar grid
|
|
const grid = document.getElementById('calendarGrid');
|
|
grid.innerHTML = '';
|
|
|
|
// Day headers - reorder based on week start day
|
|
const allDayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
|
|
const dayNames = [];
|
|
for (let i = 0; i < 7; i++) {
|
|
dayNames.push(allDayNames[(weekStartDay + i) % 7]);
|
|
}
|
|
dayNames.forEach(name => {
|
|
const header = document.createElement('div');
|
|
header.className = 'calendar-day';
|
|
header.style.cursor = 'default';
|
|
header.style.fontWeight = '600';
|
|
header.style.fontSize = '0.75rem';
|
|
header.textContent = name;
|
|
grid.appendChild(header);
|
|
});
|
|
|
|
// Empty cells before first day
|
|
for (let i = 0; i < firstDayOffset; i++) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'calendar-day';
|
|
empty.style.visibility = 'hidden';
|
|
grid.appendChild(empty);
|
|
}
|
|
|
|
// Day cells
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
const date = new Date(year, month, day);
|
|
const dateStr = utils.formatDate(date);
|
|
const dayOfWeek = date.getDay();
|
|
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
const isHoliday = utils.isItalianHoliday(date);
|
|
const isToday = date.toDateString() === today.toDateString();
|
|
const presence = presenceData[dateStr];
|
|
const parking = parkingData[dateStr];
|
|
|
|
const cell = document.createElement('div');
|
|
cell.className = 'calendar-day';
|
|
cell.dataset.date = dateStr;
|
|
|
|
if (isWeekend) cell.classList.add('weekend');
|
|
if (isHoliday) cell.classList.add('holiday');
|
|
if (isToday) cell.classList.add('today');
|
|
|
|
// Check closing days
|
|
// Note: JS getDay(): 0=Sunday, 1=Monday...
|
|
// DB WeekDay: 0=Sunday, etc. (They match)
|
|
const isWeeklyClosed = weeklyClosingDays.includes(dayOfWeek);
|
|
const isSpecificClosed = specificClosingDays.some(d => {
|
|
const start = new Date(d.date);
|
|
const end = d.end_date ? new Date(d.end_date) : start;
|
|
// Reset times for strict date comparison
|
|
start.setHours(0, 0, 0, 0);
|
|
end.setHours(0, 0, 0, 0);
|
|
return date >= start && date <= end;
|
|
});
|
|
|
|
const isClosed = isWeeklyClosed || isSpecificClosed;
|
|
|
|
if (isClosed) {
|
|
cell.classList.add('closed');
|
|
cell.title = "Ufficio Chiuso";
|
|
} else if (presence) {
|
|
cell.classList.add(`status-${presence.status}`);
|
|
}
|
|
|
|
// Show parking badge if assigned
|
|
const parkingBadge = parking
|
|
? `<span class="parking-badge">${parking.spot_display_name || parking.spot_id}</span>`
|
|
: '';
|
|
|
|
cell.innerHTML = `
|
|
<div class="day-number">${day}</div>
|
|
${parkingBadge}
|
|
`;
|
|
|
|
if (!isClosed) {
|
|
cell.addEventListener('click', () => openDayModal(dateStr, presence, parking));
|
|
}
|
|
grid.appendChild(cell);
|
|
}
|
|
}
|
|
|
|
function openDayModal(dateStr, presence, parking) {
|
|
ModalLogic.openModal({
|
|
dateStr,
|
|
presence,
|
|
parking
|
|
});
|
|
}
|
|
|
|
async function handleMarkPresence(status, date) {
|
|
const response = await api.post('/api/presence/mark', { date, status });
|
|
if (response && response.ok) {
|
|
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
|
renderCalendar();
|
|
ModalLogic.closeModal();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.detail || 'Impossibile segnare la presenza');
|
|
}
|
|
}
|
|
|
|
async function handleClearPresence(date) {
|
|
const response = await api.delete(`/api/presence/${date}`);
|
|
if (response && response.ok) {
|
|
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
|
renderCalendar();
|
|
ModalLogic.closeModal();
|
|
}
|
|
}
|
|
|
|
async function handleReleaseParking(assignmentId) {
|
|
if (!confirm('Rilasciare il parcheggio per questa data?')) return;
|
|
|
|
const response = await api.post(`/api/parking/release-my-spot/${assignmentId}`);
|
|
if (response && response.ok) {
|
|
await loadParkingAssignments();
|
|
renderCalendar();
|
|
ModalLogic.closeModal();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.detail || 'Impossibile rilasciare il parcheggio');
|
|
}
|
|
}
|
|
|
|
async function handleReassignParking(assignmentId, newUserId) {
|
|
// Basic validation handled by select; confirm
|
|
if (!assignmentId || !newUserId) {
|
|
alert('Seleziona un utente');
|
|
return;
|
|
}
|
|
|
|
const response = await api.post('/api/parking/reassign-spot', {
|
|
assignment_id: assignmentId,
|
|
new_user_id: newUserId
|
|
});
|
|
|
|
if (response && response.ok) {
|
|
await loadParkingAssignments();
|
|
renderCalendar();
|
|
ModalLogic.closeModal();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.detail || 'Impossibile riassegnare il parcheggio');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function setupEventListeners() {
|
|
// Month navigation
|
|
document.getElementById('prevMonth').addEventListener('click', async () => {
|
|
currentDate.setMonth(currentDate.getMonth() - 1);
|
|
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
|
renderCalendar();
|
|
});
|
|
|
|
document.getElementById('nextMonth').addEventListener('click', async () => {
|
|
currentDate.setMonth(currentDate.getMonth() + 1);
|
|
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
|
renderCalendar();
|
|
});
|
|
|
|
// Quick Entry Logic
|
|
const quickEntryModal = document.getElementById('quickEntryModal');
|
|
const quickEntryBtn = document.getElementById('quickEntryBtn');
|
|
const closeQuickEntryBtn = document.getElementById('closeQuickEntryModal');
|
|
const cancelQuickEntryBtn = document.getElementById('cancelQuickEntry');
|
|
const quickEntryForm = document.getElementById('quickEntryForm');
|
|
|
|
if (quickEntryBtn) {
|
|
quickEntryBtn.addEventListener('click', () => {
|
|
// Default dates: tomorrow
|
|
const tomorrow = new Date();
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
document.getElementById('qeStartDate').valueAsDate = tomorrow;
|
|
document.getElementById('qeEndDate').valueAsDate = tomorrow;
|
|
document.getElementById('qeStatus').value = '';
|
|
|
|
// Clear selections
|
|
document.querySelectorAll('.qe-status-btn').forEach(btn => btn.classList.remove('active'));
|
|
|
|
quickEntryModal.style.display = 'flex';
|
|
});
|
|
}
|
|
|
|
if (closeQuickEntryBtn) closeQuickEntryBtn.addEventListener('click', () => quickEntryModal.style.display = 'none');
|
|
if (cancelQuickEntryBtn) cancelQuickEntryBtn.addEventListener('click', () => quickEntryModal.style.display = 'none');
|
|
|
|
// Status selection in QE
|
|
document.querySelectorAll('.qe-status-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.qe-status-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById('qeStatus').value = btn.dataset.status;
|
|
});
|
|
});
|
|
|
|
if (quickEntryForm) {
|
|
quickEntryForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const startStr = document.getElementById('qeStartDate').value;
|
|
const endStr = document.getElementById('qeEndDate').value;
|
|
const status = document.getElementById('qeStatus').value;
|
|
|
|
if (!status) return utils.showMessage('Seleziona uno stato', 'error');
|
|
if (!startStr || !endStr) return utils.showMessage('Seleziona le date', 'error');
|
|
|
|
const startDate = new Date(startStr);
|
|
const endDate = new Date(endStr);
|
|
|
|
if (endDate < startDate) return utils.showMessage('La data di fine non può essere precedente alla data di inizio', 'error');
|
|
|
|
quickEntryModal.style.display = 'none';
|
|
utils.showMessage('Inserimento in corso...', 'warning');
|
|
|
|
const promises = [];
|
|
let current = new Date(startDate);
|
|
|
|
// Validate filtering
|
|
let skippedCount = 0;
|
|
|
|
while (current <= endDate) {
|
|
const dStr = current.toISOString().split('T')[0];
|
|
|
|
// Create local date for rules check (matches renderCalendar logic)
|
|
const localCurrent = new Date(dStr + 'T00:00:00');
|
|
const dayOfWeek = localCurrent.getDay(); // 0-6
|
|
|
|
// Check closing days
|
|
// Only enforce rules if we are not clearing (or should we enforce for clearing too?
|
|
// Usually clearing is allowed always, but "Inserimento" implies adding.
|
|
// Ensuring we don't ADD presence on closed days is the main goal.)
|
|
let isClosed = false;
|
|
|
|
if (status !== 'clear') {
|
|
const isWeeklyClosed = weeklyClosingDays.includes(dayOfWeek);
|
|
const isSpecificClosed = specificClosingDays.some(d => {
|
|
const start = new Date(d.date);
|
|
const end = d.end_date ? new Date(d.end_date) : start;
|
|
|
|
start.setHours(0, 0, 0, 0);
|
|
end.setHours(0, 0, 0, 0);
|
|
|
|
// localCurrent is already set to 00:00:00 local
|
|
return localCurrent >= start && localCurrent <= end;
|
|
});
|
|
|
|
if (isWeeklyClosed || isSpecificClosed) isClosed = true;
|
|
}
|
|
|
|
if (isClosed) {
|
|
skippedCount++;
|
|
} else {
|
|
if (status === 'clear') {
|
|
promises.push(api.delete(`/api/presence/${dStr}`));
|
|
} else {
|
|
promises.push(api.post('/api/presence/mark', { date: dStr, status: status }));
|
|
}
|
|
}
|
|
current.setDate(current.getDate() + 1);
|
|
}
|
|
|
|
try {
|
|
await Promise.all(promises);
|
|
if (skippedCount > 0) {
|
|
utils.showMessage(`Inserimento completato! (${skippedCount} giorni chiusi ignorati)`, 'warning');
|
|
} else {
|
|
utils.showMessage('Inserimento completato!', 'success');
|
|
}
|
|
await Promise.all([loadPresences(), loadParkingAssignments()]);
|
|
renderCalendar();
|
|
} catch (err) {
|
|
console.error(err);
|
|
utils.showMessage('Errore durante l\'inserimento. Alcuni giorni potrebbero non essere stati aggiornati.', 'error');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Parking Status Logic
|
|
// ----------------------------------------------------------------------------
|
|
|
|
function initParkingStatus() {
|
|
updateStatusHeader();
|
|
loadDailyStatus();
|
|
|
|
// Update office name if available
|
|
if (currentUser && currentUser.office_name) {
|
|
const nameDisplay = document.getElementById('statusOfficeName');
|
|
if (nameDisplay) nameDisplay.textContent = currentUser.office_name;
|
|
|
|
const headerDisplay = document.getElementById('currentOfficeDisplay');
|
|
if (headerDisplay) headerDisplay.textContent = currentUser.office_name;
|
|
} else {
|
|
const nameDisplay = document.getElementById('statusOfficeName');
|
|
if (nameDisplay) nameDisplay.textContent = 'Tuo Ufficio';
|
|
|
|
const headerDisplay = document.getElementById('currentOfficeDisplay');
|
|
if (headerDisplay) headerDisplay.textContent = 'Tuo Ufficio';
|
|
}
|
|
}
|
|
|
|
function updateStatusHeader() {
|
|
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
|
const dateStr = statusDate.toLocaleDateString('it-IT', options);
|
|
const capitalizedDate = dateStr.charAt(0).toUpperCase() + dateStr.slice(1);
|
|
|
|
const statusDateDisplay = document.getElementById('statusDateDisplay');
|
|
if (statusDateDisplay) statusDateDisplay.textContent = capitalizedDate;
|
|
|
|
const pickerDateDisplay = document.getElementById('pickerDateDisplay');
|
|
if (pickerDateDisplay) pickerDateDisplay.textContent = utils.formatDate(statusDate);
|
|
|
|
const summaryDateDisplay = document.getElementById('summaryDateDisplay');
|
|
if (summaryDateDisplay) summaryDateDisplay.textContent = dateStr;
|
|
|
|
const picker = document.getElementById('statusDatePicker');
|
|
if (picker) {
|
|
const yyyy = statusDate.getFullYear();
|
|
const mm = String(statusDate.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(statusDate.getDate()).padStart(2, '0');
|
|
picker.value = `${yyyy}-${mm}-${dd}`;
|
|
}
|
|
}
|
|
|
|
async function loadDailyStatus() {
|
|
if (!currentUser || !currentUser.office_id) return;
|
|
|
|
const dateStr = utils.formatDate(statusDate);
|
|
const officeId = currentUser.office_id;
|
|
|
|
const grid = document.getElementById('spotsGrid');
|
|
// Keep grid height to avoid jump if possible, or just loading styling
|
|
if (grid) grid.innerHTML = '<div style="width:100%; text-align:center; padding:2rem; color:var(--text-secondary);">Caricamento...</div>';
|
|
|
|
try {
|
|
const response = await api.get(`/api/parking/assignments/${dateStr}?office_id=${officeId}`);
|
|
if (response && response.ok) {
|
|
const assignments = await response.json();
|
|
renderParkingStatus(assignments);
|
|
} else {
|
|
if (grid) grid.innerHTML = '<div style="width:100%; text-align:center; padding:1rem;">Impossibile caricare i dati.</div>';
|
|
}
|
|
} catch (e) {
|
|
console.error("Error loading parking status", e);
|
|
if (grid) grid.innerHTML = '<div style="width:100%; text-align:center; padding:1rem;">Errore di caricamento.</div>';
|
|
}
|
|
}
|
|
|
|
function renderParkingStatus(assignments) {
|
|
const grid = document.getElementById('spotsGrid');
|
|
if (!grid) return;
|
|
|
|
grid.innerHTML = '';
|
|
|
|
if (!assignments || assignments.length === 0) {
|
|
grid.innerHTML = '<div style="width:100%; text-align:center; padding:1rem;">Nessun posto configurato o disponibile.</div>';
|
|
const badge = document.getElementById('spotsCountBadge');
|
|
if (badge) badge.textContent = `Liberi: 0/0`;
|
|
return;
|
|
}
|
|
|
|
// Sort
|
|
assignments.sort((a, b) => {
|
|
const nameA = a.spot_display_name || a.spot_id;
|
|
const nameB = b.spot_display_name || b.spot_id;
|
|
return nameA.localeCompare(nameB, undefined, { numeric: true, sensitivity: 'base' });
|
|
});
|
|
|
|
let total = assignments.length;
|
|
let free = 0;
|
|
|
|
assignments.forEach(a => {
|
|
const isFree = !a.user_id;
|
|
if (isFree) free++;
|
|
|
|
const spotName = a.spot_display_name || a.spot_id;
|
|
const statusText = isFree ? 'Libero' : (a.user_name || 'Occupato');
|
|
|
|
// Colors: Free = Green (default), Occupied = Yellow (requested)
|
|
// Yellow palette: Border #eab308, bg #fefce8, text #a16207, icon #eab308
|
|
|
|
const borderColor = isFree ? '#22c55e' : '#eab308';
|
|
const bgColor = isFree ? '#f0fdf4' : '#fefce8';
|
|
const textColor = isFree ? '#15803d' : '#a16207';
|
|
const iconColor = isFree ? '#22c55e' : '#eab308';
|
|
|
|
const el = document.createElement('div');
|
|
el.className = 'spot-card';
|
|
el.style.cssText = `
|
|
border: 1px solid ${borderColor};
|
|
background: ${bgColor};
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
width: 140px;
|
|
min-width: 120px;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
transition: all 0.2s;
|
|
`;
|
|
|
|
// New Car Icon (Front Facing Sedan style or similar simple shape)
|
|
// Using a cleaner SVG path
|
|
el.innerHTML = `
|
|
<div style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">${spotName}</div>
|
|
<div style="color: ${textColor}; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;" title="${statusText}">
|
|
${statusText}
|
|
</div>
|
|
`;
|
|
|
|
grid.appendChild(el);
|
|
});
|
|
|
|
const badge = document.getElementById('spotsCountBadge');
|
|
if (badge) badge.textContent = `Liberi: ${free}/${total}`;
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupStatusListeners() {
|
|
const prevDay = document.getElementById('statusPrevDay');
|
|
if (prevDay) prevDay.addEventListener('click', () => {
|
|
statusDate.setDate(statusDate.getDate() - 1);
|
|
updateStatusHeader();
|
|
loadDailyStatus();
|
|
});
|
|
|
|
const nextDay = document.getElementById('statusNextDay');
|
|
if (nextDay) nextDay.addEventListener('click', () => {
|
|
statusDate.setDate(statusDate.getDate() + 1);
|
|
updateStatusHeader();
|
|
loadDailyStatus();
|
|
});
|
|
|
|
const datePicker = document.getElementById('statusDatePicker');
|
|
if (datePicker) datePicker.addEventListener('change', (e) => {
|
|
if (e.target.value) {
|
|
statusDate = new Date(e.target.value);
|
|
updateStatusHeader();
|
|
loadDailyStatus();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Exclusion Logic
|
|
// ----------------------------------------------------------------------------
|
|
|
|
async function initExclusionLogic() {
|
|
await loadExclusionStatus();
|
|
setupExclusionListeners();
|
|
}
|
|
|
|
async function loadExclusionStatus() {
|
|
try {
|
|
const response = await api.get('/api/users/me/exclusion');
|
|
if (response && response.ok) {
|
|
const data = await response.json();
|
|
updateExclusionUI(data); // data is now a list
|
|
}
|
|
} catch (e) {
|
|
console.error("Error loading exclusion status", e);
|
|
}
|
|
}
|
|
|
|
function updateExclusionUI(exclusions) {
|
|
const statusDiv = document.getElementById('exclusionStatusDisplay');
|
|
const manageBtn = document.getElementById('manageExclusionBtn');
|
|
|
|
// Always show manage button as "Aggiungi Esclusione"
|
|
manageBtn.textContent = 'Aggiungi Esclusione';
|
|
// Clear previous binding to avoid duplicates or simply use a new function
|
|
// But specific listeners are set in setupExclusionListeners.
|
|
// Actually, manageBtn logic was resetting UI.
|
|
|
|
if (exclusions && exclusions.length > 0) {
|
|
statusDiv.style.display = 'block';
|
|
|
|
let html = '<div style="display:flex; flex-direction:column; gap:0.5rem;">';
|
|
|
|
exclusions.forEach(ex => {
|
|
let period = 'Tempo Indeterminato';
|
|
if (ex.start_date && ex.end_date) {
|
|
period = `${utils.formatDate(new Date(ex.start_date))} - ${utils.formatDate(new Date(ex.end_date))}`;
|
|
} else if (ex.start_date) {
|
|
period = `Dal ${utils.formatDate(new Date(ex.start_date))}`;
|
|
}
|
|
|
|
html += `
|
|
<div style="background: white; border: 1px solid #e5e7eb; padding: 0.75rem; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
|
<div>
|
|
<div style="font-weight: 500; font-size: 0.9rem;">${period}</div>
|
|
${ex.notes ? `<div style="font-size: 0.8rem; color: #6b7280;">${ex.notes}</div>` : ''}
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button class="btn-icon" onclick='openEditMyExclusion("${ex.id}", ${JSON.stringify(ex).replace(/'/g, "'")})' title="Modifica">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
|
</svg>
|
|
</button>
|
|
<button class="btn-icon btn-danger" onclick="deleteMyExclusion('${ex.id}')" title="Rimuovi">
|
|
×
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
|
|
html += '</div>';
|
|
statusDiv.innerHTML = html;
|
|
|
|
// Update container style for list
|
|
statusDiv.style.backgroundColor = '#f9fafb';
|
|
statusDiv.style.color = 'inherit';
|
|
statusDiv.style.border = 'none'; // remove border from container, items have border
|
|
statusDiv.style.padding = '0'; // reset padding
|
|
|
|
} else {
|
|
statusDiv.style.display = 'none';
|
|
statusDiv.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Global for edit
|
|
let myEditingExclusionId = null;
|
|
|
|
function openEditMyExclusion(id, data) {
|
|
myEditingExclusionId = id;
|
|
const modal = document.getElementById('userExclusionModal');
|
|
const radioForever = document.querySelector('input[name="exclusionType"][value="forever"]');
|
|
const radioRange = document.querySelector('input[name="exclusionType"][value="range"]');
|
|
const rangeDiv = document.getElementById('exclusionDateRange');
|
|
const deleteBtn = document.getElementById('deleteExclusionBtn'); // Hide in edit mode (we have icon) or keep?
|
|
// User requested "matita a destra per la modifica ed eliminazione".
|
|
// I added trash icon to the list. So modal "Rimuovi" is redundant but harmless.
|
|
// I'll hide it for clarity.
|
|
if (deleteBtn) deleteBtn.style.display = 'none';
|
|
|
|
if (data.start_date || data.end_date) {
|
|
radioRange.checked = true;
|
|
rangeDiv.style.display = 'block';
|
|
if (data.start_date) document.getElementById('ueStartDate').value = data.start_date;
|
|
if (data.end_date) document.getElementById('ueEndDate').value = data.end_date;
|
|
} else {
|
|
radioForever.checked = true;
|
|
rangeDiv.style.display = 'none';
|
|
document.getElementById('ueStartDate').value = '';
|
|
document.getElementById('ueEndDate').value = '';
|
|
}
|
|
document.getElementById('ueNotes').value = data.notes || '';
|
|
|
|
document.querySelector('#userExclusionModal h3').textContent = 'Modifica Esclusione';
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
async function deleteMyExclusion(id) {
|
|
if (!confirm('Rimuovere questa esclusione?')) return;
|
|
const response = await api.delete(`/api/users/me/exclusion/${id}`);
|
|
if (response && response.ok) {
|
|
utils.showMessage('Esclusione rimossa con successo', 'success');
|
|
loadExclusionStatus();
|
|
} else {
|
|
const err = await response.json();
|
|
utils.showMessage(err.detail || 'Errore rimozione', 'error');
|
|
}
|
|
}
|
|
|
|
function resetMyExclusionForm() {
|
|
document.getElementById('userExclusionForm').reset();
|
|
myEditingExclusionId = null;
|
|
document.querySelector('#userExclusionModal h3').textContent = 'Nuova Esclusione';
|
|
|
|
const rangeDiv = document.getElementById('exclusionDateRange');
|
|
rangeDiv.style.display = 'none';
|
|
document.querySelector('input[name="exclusionType"][value="forever"]').checked = true;
|
|
|
|
// Hide delete btn in modal (using list icon instead)
|
|
const deleteBtn = document.getElementById('deleteExclusionBtn');
|
|
if (deleteBtn) deleteBtn.style.display = 'none';
|
|
}
|
|
|
|
function setupExclusionListeners() {
|
|
const modal = document.getElementById('userExclusionModal');
|
|
const manageBtn = document.getElementById('manageExclusionBtn');
|
|
const closeBtn = document.getElementById('closeUserExclusionModal');
|
|
const cancelBtn = document.getElementById('cancelUserExclusion');
|
|
const form = document.getElementById('userExclusionForm');
|
|
|
|
const radioForever = document.querySelector('input[name="exclusionType"][value="forever"]');
|
|
const radioRange = document.querySelector('input[name="exclusionType"][value="range"]');
|
|
const rangeDiv = document.getElementById('exclusionDateRange');
|
|
|
|
if (manageBtn) {
|
|
manageBtn.addEventListener('click', () => {
|
|
resetMyExclusionForm();
|
|
modal.style.display = 'flex';
|
|
});
|
|
}
|
|
|
|
if (closeBtn) closeBtn.addEventListener('click', () => modal.style.display = 'none');
|
|
if (cancelBtn) cancelBtn.addEventListener('click', () => modal.style.display = 'none');
|
|
|
|
// Radio logic
|
|
radioForever.addEventListener('change', () => rangeDiv.style.display = 'none');
|
|
radioRange.addEventListener('change', () => rangeDiv.style.display = 'block');
|
|
|
|
// Save
|
|
if (form) {
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const type = document.querySelector('input[name="exclusionType"]:checked').value;
|
|
const payload = {
|
|
notes: document.getElementById('ueNotes').value
|
|
};
|
|
|
|
if (type === 'range') {
|
|
const start = document.getElementById('ueStartDate').value;
|
|
const end = document.getElementById('ueEndDate').value;
|
|
|
|
if (start) payload.start_date = start;
|
|
if (end) payload.end_date = end;
|
|
|
|
if (start && end && new Date(end) < new Date(start)) {
|
|
return utils.showMessage('La data di fine deve essere dopo la data di inizio', 'error');
|
|
}
|
|
} else {
|
|
payload.start_date = null;
|
|
payload.end_date = null;
|
|
}
|
|
|
|
let response;
|
|
if (myEditingExclusionId) {
|
|
response = await api.put(`/api/users/me/exclusion/${myEditingExclusionId}`, payload);
|
|
} else {
|
|
response = await api.post('/api/users/me/exclusion', payload);
|
|
}
|
|
|
|
if (response && response.ok) {
|
|
utils.showMessage('Esclusione salvata', 'success');
|
|
modal.style.display = 'none';
|
|
loadExclusionStatus();
|
|
} else {
|
|
const err = await response.json();
|
|
utils.showMessage(err.detail || 'Errore salvataggio', 'error');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Globals
|
|
window.openEditMyExclusion = openEditMyExclusion;
|
|
window.deleteMyExclusion = deleteMyExclusion;
|