aggiunti trasferte, export excel, miglioramenti generali
This commit is contained in:
@@ -652,6 +652,11 @@ textarea {
|
||||
border-color: var(--danger) !important;
|
||||
}
|
||||
|
||||
.status-business_trip {
|
||||
background: var(--warning-bg) !important;
|
||||
border-color: var(--warning) !important;
|
||||
}
|
||||
|
||||
.status-nodata {
|
||||
background: white;
|
||||
}
|
||||
@@ -1788,6 +1793,7 @@ textarea {
|
||||
transform: translate(-50%, 100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(-50%, 0);
|
||||
opacity: 1;
|
||||
@@ -1798,6 +1804,7 @@ textarea {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -185,9 +185,23 @@ const api = {
|
||||
* Logout
|
||||
*/
|
||||
async logout() {
|
||||
// Fetch config to check for external logout URL
|
||||
let logoutUrl = '/login';
|
||||
try {
|
||||
const configRes = await this.get('/api/auth/config');
|
||||
if (configRes && configRes.ok) {
|
||||
const config = await configRes.json();
|
||||
if (config.authelia_enabled && config.logout_url) {
|
||||
logoutUrl = config.logout_url;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching logout config', e);
|
||||
}
|
||||
|
||||
await this.post('/api/auth/logout', {});
|
||||
this.clearToken();
|
||||
window.location.href = '/login';
|
||||
window.location.href = logoutUrl;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -87,9 +87,6 @@ async function initNav() {
|
||||
// Get user info (works with both JWT and Authelia)
|
||||
const currentUser = await api.checkAuth();
|
||||
|
||||
// Render navigation
|
||||
navContainer.innerHTML = renderNav(currentPath, currentUser?.role);
|
||||
|
||||
// Update user info in sidebar
|
||||
if (currentUser) {
|
||||
const userNameEl = document.getElementById('userName');
|
||||
@@ -98,11 +95,55 @@ async function initNav() {
|
||||
if (userRoleEl) userRoleEl.textContent = currentUser.role || '-';
|
||||
}
|
||||
|
||||
// Setup user menu
|
||||
// Setup user menu (logout) & mobile menu
|
||||
setupUserMenu();
|
||||
|
||||
// Setup mobile menu
|
||||
setupMobileMenu();
|
||||
|
||||
// CHECK: Block access if user has no office (and is not admin)
|
||||
// Admins are allowed to access "Gestione Uffici" even without an office
|
||||
if (currentUser && !currentUser.office_id && currentUser.role !== 'admin') {
|
||||
navContainer.innerHTML = ''; // Clear nav
|
||||
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
if (mainContent) {
|
||||
mainContent.innerHTML = `
|
||||
<div style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80vh;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
">
|
||||
<div class="card" style="max-width: 500px; padding: 2.5rem; border-top: 4px solid #ef4444;">
|
||||
<div style="color: #ef4444; margin-bottom: 1.5rem;">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style="margin-bottom: 1rem;">Ufficio non assegnato</h2>
|
||||
<p class="text-secondary" style="margin-bottom: 1.5rem; line-height: 1.6;">
|
||||
Il tuo account <strong>${currentUser.email}</strong> è attivo, ma non sei ancora stato assegnato a nessuno ufficio.
|
||||
</p>
|
||||
<div style="padding: 1.5rem; background: #f8fafc; border-radius: 8px; text-align: left;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text);">Cosa fare?</div>
|
||||
<div style="font-size: 0.95rem; color: var(--text-secondary);">
|
||||
Contatta l'amministratore di sistema per richiedere l'assegnazione al tuo ufficio di competenza.<br>
|
||||
<a href="mailto:s.salemi@sielte.it" style="color: var(--primary); text-decoration: none; font-weight: 500; margin-top: 0.5rem; display: inline-block;">s.salemi@sielte.it</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return; // STOP rendering nav
|
||||
}
|
||||
|
||||
// Render navigation (Normal Flow)
|
||||
navContainer.innerHTML = renderNav(currentPath, currentUser?.role);
|
||||
}
|
||||
|
||||
function setupMobileMenu() {
|
||||
|
||||
@@ -143,6 +143,7 @@ function setupEventListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
// Test Tools
|
||||
// Test Tools
|
||||
document.getElementById('runAllocationBtn').addEventListener('click', async () => {
|
||||
if (!confirm('Sei sicuro di voler avviare l\'assegnazione ORA? Questo potrebbe sovrascrivere le assegnazioni esistenti per la data selezionata.')) return;
|
||||
@@ -166,7 +167,7 @@ function setupEventListeners() {
|
||||
utils.showMessage('Avvio assegnazione...', 'success');
|
||||
|
||||
while (current <= end) {
|
||||
const dateStr = current.toISOString().split('T')[0];
|
||||
const dateStr = utils.formatDate(current);
|
||||
try {
|
||||
await api.post('/api/parking/run-allocation', {
|
||||
date: dateStr,
|
||||
@@ -207,20 +208,110 @@ function setupEventListeners() {
|
||||
|
||||
utils.showMessage('Rimozione in corso...', 'warning');
|
||||
|
||||
// Loop is fine, but maybe redundant if we could batch clean?
|
||||
// Backend clear-assignments is per day.
|
||||
while (current <= end) {
|
||||
const dateStr = current.toISOString().split('T')[0];
|
||||
const dateStr = utils.formatDate(current);
|
||||
try {
|
||||
const res = await api.post('/api/parking/clear-assignments', {
|
||||
date: dateStr,
|
||||
office_id: currentOffice.id
|
||||
});
|
||||
totalRemoved += (res.count || 0);
|
||||
if (res && res.ok) {
|
||||
const data = await res.json();
|
||||
totalRemoved += (data.count || 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error clearing ${dateStr}`, e);
|
||||
}
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
utils.showMessage(`Operazione eseguita. ${totalRemoved} assegnazioni rimosse in totale.`, 'warning');
|
||||
utils.showMessage(`Operazione eseguita.`, 'warning');
|
||||
});
|
||||
|
||||
const clearPresenceBtn = document.getElementById('clearPresenceBtn');
|
||||
if (clearPresenceBtn) {
|
||||
clearPresenceBtn.addEventListener('click', async () => {
|
||||
if (!confirm('ATTENZIONE: Stai per eliminare TUTTI GLI STATI (Presente/Assente/ecc) e relative assegnazioni per tutti gli utenti dell\'ufficio nel periodo selezionato. \n\nQuesta azione è irreversibile. Procedere?')) return;
|
||||
|
||||
const dateStart = document.getElementById('testDateStart').value;
|
||||
const dateEnd = document.getElementById('testDateEnd').value;
|
||||
|
||||
if (!dateStart) return utils.showMessage('Seleziona una data di inizio', 'error');
|
||||
|
||||
// Validate office
|
||||
if (!currentOffice || !currentOffice.id) {
|
||||
return utils.showMessage('Errore: Nessun ufficio selezionato', 'error');
|
||||
}
|
||||
|
||||
const endDateVal = dateEnd || dateStart;
|
||||
|
||||
utils.showMessage('Rimozione stati in corso...', 'warning');
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/presence/admin/clear-office-presence', {
|
||||
start_date: dateStart,
|
||||
end_date: endDateVal,
|
||||
office_id: currentOffice.id
|
||||
});
|
||||
|
||||
if (res && res.ok) {
|
||||
const data = await res.json();
|
||||
utils.showMessage(`Operazione completata. Rimossi ${data.count_presence} stati e ${data.count_parking} assegnazioni.`, 'success');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
utils.showMessage('Errore: ' + (err.detail || 'Operazione fallita'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
utils.showMessage('Errore di comunicazione col server', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const testEmailBtn = document.getElementById('testEmailBtn');
|
||||
if (testEmailBtn) {
|
||||
testEmailBtn.addEventListener('click', async () => {
|
||||
const dateVal = document.getElementById('testEmailDate').value;
|
||||
|
||||
// Validate office
|
||||
if (!currentOffice || !currentOffice.id) {
|
||||
return utils.showMessage('Errore: Nessun ufficio selezionato', 'error');
|
||||
}
|
||||
|
||||
utils.showMessage('Invio mail di test in corso...', 'warning');
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/parking/test-email', {
|
||||
date: dateVal || null,
|
||||
office_id: currentOffice.id
|
||||
});
|
||||
|
||||
if (res && res.status !== 403 && res.status !== 500 && res.ok !== false) {
|
||||
// API wrapper usually returns response object or parses JSON?
|
||||
// api.post returns response object if 200-299, but wrapper handles some.
|
||||
// Let's assume standard fetch response or check wrapper.
|
||||
// api.js Wrapper returns fetch Response.
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
let msg = `Email inviata con successo per la data: ${data.date}.`;
|
||||
if (data.mode === 'FILE') {
|
||||
msg += ' (SMTP disabilitato: Loggato su file)';
|
||||
}
|
||||
utils.showMessage(msg, 'success');
|
||||
} else {
|
||||
utils.showMessage('Invio fallito. Controlla i log del server.', 'error');
|
||||
}
|
||||
} else {
|
||||
const err = res ? await res.json() : {};
|
||||
utils.showMessage('Errore: ' + (err.detail || 'Invio fallito'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
utils.showMessage('Errore di comunicazione col server', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize Parking Status
|
||||
initParkingStatus();
|
||||
setupStatusListeners();
|
||||
|
||||
// Initialize Exclusion Logic
|
||||
initExclusionLogic();
|
||||
});
|
||||
|
||||
async function loadPresences() {
|
||||
@@ -337,19 +340,57 @@ function setupEventListeners() {
|
||||
const promises = [];
|
||||
let current = new Date(startDate);
|
||||
|
||||
// Validate filtering
|
||||
let skippedCount = 0;
|
||||
|
||||
while (current <= endDate) {
|
||||
const dStr = current.toISOString().split('T')[0];
|
||||
if (status === 'clear') {
|
||||
promises.push(api.delete(`/api/presence/${dStr}`));
|
||||
|
||||
// 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 {
|
||||
promises.push(api.post('/api/presence/mark', { date: dStr, status: status }));
|
||||
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);
|
||||
utils.showMessage('Inserimento completato!', 'success');
|
||||
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) {
|
||||
@@ -531,3 +572,211 @@ function setupStatusListeners() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
@@ -113,6 +113,77 @@ async function loadOffices() {
|
||||
|
||||
// Initial update of office display
|
||||
updateOfficeDisplay();
|
||||
|
||||
// Show export card for Admin/Manager
|
||||
if (['admin', 'manager'].includes(currentUser.role)) {
|
||||
const exportCard = document.getElementById('exportCard');
|
||||
if (exportCard) {
|
||||
exportCard.style.display = 'block';
|
||||
|
||||
// Set defaults (current month)
|
||||
const today = new Date();
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
|
||||
document.getElementById('exportStartDate').valueAsDate = firstDay;
|
||||
document.getElementById('exportEndDate').valueAsDate = lastDay;
|
||||
|
||||
document.getElementById('exportBtn').addEventListener('click', handleExport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const startStr = document.getElementById('exportStartDate').value;
|
||||
const endStr = document.getElementById('exportEndDate').value;
|
||||
const officeId = document.getElementById('officeFilter').value;
|
||||
|
||||
if (!startStr || !endStr) {
|
||||
alert('Seleziona le date di inizio e fine');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct URL
|
||||
let url = `/api/reports/team-export?start_date=${startStr}&end_date=${endStr}`;
|
||||
if (officeId) {
|
||||
url += `&office_id=${officeId}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const btn = document.getElementById('exportBtn');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generazione...';
|
||||
|
||||
// Use fetch directly to handle blob
|
||||
const token = api.getToken();
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = `report_presenze_${startStr}_${endStr}.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
a.remove();
|
||||
} else {
|
||||
const err = await response.json();
|
||||
alert('Errore export: ' + (err.detail || 'Sconosciuto'));
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Errore durante l\'export');
|
||||
document.getElementById('exportBtn').disabled = false;
|
||||
document.getElementById('exportBtn').textContent = 'Esporta Excel';
|
||||
}
|
||||
}
|
||||
|
||||
function getDateRange() {
|
||||
|
||||
@@ -267,26 +267,70 @@ async function loadExclusions(officeId) {
|
||||
</span>
|
||||
${e.notes ? `<span class="rule-note">${e.notes}</span>` : ''}
|
||||
</div>
|
||||
<button class="btn-icon btn-danger" onclick="deleteExclusion('${e.id}')">
|
||||
×
|
||||
</button>
|
||||
<div class="rule-actions" style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button class="btn-icon" onclick='openEditExclusion("${e.id}", ${JSON.stringify(e).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="deleteExclusion('${e.id}')" title="Elimina">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
async function addExclusion(data) {
|
||||
const response = await api.post(`/api/offices/${currentOfficeId}/exclusions`, data);
|
||||
// Global variable to track edit mode
|
||||
let editingExclusionId = null;
|
||||
|
||||
async function openEditExclusion(id, data) {
|
||||
editingExclusionId = id;
|
||||
|
||||
// Populate form
|
||||
populateUserSelects();
|
||||
document.getElementById('exclusionUser').value = data.user_id;
|
||||
// Disable user select in edit mode usually? Or allow change? API allows it.
|
||||
|
||||
document.getElementById('exclusionStartDate').value = data.start_date || '';
|
||||
document.getElementById('exclusionEndDate').value = data.end_date || '';
|
||||
document.getElementById('exclusionNotes').value = data.notes || '';
|
||||
|
||||
// Change modal title/button
|
||||
document.querySelector('#exclusionModal h3').textContent = 'Modifica Esclusione';
|
||||
document.querySelector('#exclusionForm button[type="submit"]').textContent = 'Salva Modifiche';
|
||||
|
||||
document.getElementById('exclusionModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function saveExclusion(data) {
|
||||
let response;
|
||||
if (editingExclusionId) {
|
||||
response = await api.put(`/api/offices/${currentOfficeId}/exclusions/${editingExclusionId}`, data);
|
||||
} else {
|
||||
response = await api.post(`/api/offices/${currentOfficeId}/exclusions`, data);
|
||||
}
|
||||
|
||||
if (response && response.ok) {
|
||||
await loadExclusions(currentOfficeId);
|
||||
document.getElementById('exclusionModal').style.display = 'none';
|
||||
document.getElementById('exclusionForm').reset();
|
||||
resetExclusionForm();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Impossibile aggiungere l\'esclusione');
|
||||
alert(error.detail || 'Impossibile salvare l\'esclusione');
|
||||
}
|
||||
}
|
||||
|
||||
function resetExclusionForm() {
|
||||
document.getElementById('exclusionForm').reset();
|
||||
editingExclusionId = null;
|
||||
document.querySelector('#exclusionModal h3').textContent = 'Aggiungi Esclusione Parcheggio';
|
||||
document.querySelector('#exclusionForm button[type="submit"]').textContent = 'Aggiungi';
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function deleteExclusion(id) {
|
||||
if (!confirm('Eliminare questa esclusione?')) return;
|
||||
const response = await api.delete(`/api/offices/${currentOfficeId}/exclusions/${id}`);
|
||||
@@ -335,6 +379,10 @@ function setupEventListeners() {
|
||||
modals.forEach(m => {
|
||||
document.getElementById(m.btn).addEventListener('click', () => {
|
||||
if (m.id !== 'closingDayModal') populateUserSelects();
|
||||
|
||||
// Special handling for exclusion to reset edit mode
|
||||
if (m.id === 'exclusionModal') resetExclusionForm();
|
||||
|
||||
document.getElementById(m.id).style.display = 'flex';
|
||||
});
|
||||
document.getElementById(m.close).addEventListener('click', () => {
|
||||
@@ -368,7 +416,7 @@ function setupEventListeners() {
|
||||
|
||||
document.getElementById('exclusionForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
addExclusion({
|
||||
saveExclusion({
|
||||
user_id: document.getElementById('exclusionUser').value,
|
||||
start_date: document.getElementById('exclusionStartDate').value || null,
|
||||
end_date: document.getElementById('exclusionEndDate').value || null,
|
||||
@@ -380,4 +428,7 @@ function setupEventListeners() {
|
||||
// Global functions
|
||||
window.deleteClosingDay = deleteClosingDay;
|
||||
window.deleteGuarantee = deleteGuarantee;
|
||||
window.deleteClosingDay = deleteClosingDay;
|
||||
window.deleteGuarantee = deleteGuarantee;
|
||||
window.deleteExclusion = deleteExclusion;
|
||||
window.openEditExclusion = openEditExclusion;
|
||||
|
||||
@@ -138,6 +138,28 @@
|
||||
<button id="clearAssignmentsBtn" class="btn btn-danger">
|
||||
Elimina Tutte le Assegnazioni
|
||||
</button>
|
||||
<button id="clearPresenceBtn" class="btn btn-danger"
|
||||
title="Elimina stati e assegnazioni per i giorni selezionati">
|
||||
Elimina Stati
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr style="margin: 1.5rem 0; border: 0; border-top: 1px solid #e5e7eb;">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Test Invio Email</label>
|
||||
<div style="display: flex; gap: 1rem; align-items: flex-end;">
|
||||
<div style="flex: 1;">
|
||||
<small>Data di Riferimento (Opzionale):</small>
|
||||
<input type="date" id="testEmailDate" class="form-control">
|
||||
<small class="text-muted" style="display: block; margin-top: 0.25rem;">
|
||||
Se non specificata, verrà usato il primo giorno lavorativo disponibile.
|
||||
</small>
|
||||
</div>
|
||||
<button id="testEmailBtn" class="btn btn-secondary">
|
||||
Test Invio Mail
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,11 @@
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-absent"></div>
|
||||
<span>Assente</span>
|
||||
<span>Ferie</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-business_trip"></div>
|
||||
<span>Trasferta</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,11 +159,35 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Exclusion Card -->
|
||||
<div class="card" id="exclusionCard" style="margin-top: 2rem;">
|
||||
<div class="card-header">
|
||||
<h3>Esclusione Assegnazione</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">
|
||||
Puoi decidere di escluderti automaticamente dalla logica di assegnazione dei posti auto.
|
||||
Le richieste di esclusione sono visibili agli amministratori.
|
||||
</p>
|
||||
|
||||
<div id="exclusionStatusDisplay"
|
||||
style="display: none; padding: 1rem; border-radius: 6px; margin-bottom: 1rem; background-color: #f3f4f6; border: 1px solid #e5e7eb;">
|
||||
<!-- Filled by JS -->
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-dark" id="manageExclusionBtn">Gestisci Esclusione</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card parking-map-card" style="margin-top: 2rem;">
|
||||
<h3>Mappa Parcheggio</h3>
|
||||
<img src="/assets/parking-map.png" alt="Mappa Parcheggio"
|
||||
style="width: 100%; height: auto; border-radius: 4px; border: 1px solid var(--border);">
|
||||
</div>
|
||||
</div> <!-- End parking-map-card -->
|
||||
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -199,7 +227,11 @@
|
||||
</button>
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Assente</span>
|
||||
<span>Ferie</span>
|
||||
</button>
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="business_trip">
|
||||
<div class="status-icon status-business_trip"></div>
|
||||
<span>Trasferta</span>
|
||||
</button>
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="clear">
|
||||
<div class="status-icon"
|
||||
@@ -241,7 +273,11 @@
|
||||
</button>
|
||||
<button class="status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Assente</span>
|
||||
<span>Ferie</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="business_trip">
|
||||
<div class="status-icon status-business_trip"></div>
|
||||
<span>Trasferta</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -276,6 +312,59 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Exclusion Modal -->
|
||||
<div class="modal" id="userExclusionModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Gestisci Esclusione</h3>
|
||||
<button class="modal-close" id="closeUserExclusionModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="userExclusionForm">
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 500; margin-bottom: 0.5rem; display: block;">Durata
|
||||
Esclusione</label>
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="radio" name="exclusionType" value="forever" checked>
|
||||
<span>Tempo Indeterminato</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="radio" name="exclusionType" value="range">
|
||||
<span>Periodo Specifico</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exclusionDateRange" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="ueStartDate">Data Inizio</label>
|
||||
<input type="date" id="ueStartDate" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ueEndDate">Data Fine</label>
|
||||
<input type="date" id="ueEndDate" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ueNotes">Motivo (opzionale)</label>
|
||||
<textarea id="ueNotes" class="form-control" rows="2"
|
||||
placeholder="Es. Lavoro da remoto per un mese..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-danger" id="deleteExclusionBtn"
|
||||
style="display: none; margin-right: auto;">Rimuovi</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelUserExclusion">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@
|
||||
<small class="text-muted">Il ruolo è assegnato dal tuo amministratore</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manager">Manager</label>
|
||||
<label for="manager">Ufficio</label>
|
||||
<input type="text" id="manager" disabled>
|
||||
<small class="text-muted">Il tuo manager è assegnato dall'amministratore</small>
|
||||
<small class="text-muted">Il tuo ufficio è assegnato dall'amministratore</small>
|
||||
</div>
|
||||
<div class="form-actions" id="profileActions">
|
||||
<button type="submit" class="btn btn-dark">Salva Modifiche</button>
|
||||
@@ -139,7 +139,7 @@
|
||||
document.getElementById('name').value = profile.name || '';
|
||||
document.getElementById('email').value = profile.email;
|
||||
document.getElementById('role').value = profile.role;
|
||||
document.getElementById('manager').value = profile.manager_name || 'Nessuno';
|
||||
document.getElementById('manager').value = profile.office_name || 'Nessuno';
|
||||
|
||||
// LDAP mode adjustments
|
||||
if (isLdapUser) {
|
||||
|
||||
@@ -54,17 +54,7 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="notificationForm">
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<span>Riepilogo Settimanale</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="notifyWeeklyParking">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<small class="text-muted">Ricevi il riepilogo settimanale delle assegnazioni parcheggio ogni
|
||||
Venerdì alle 12:00</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<span>Promemoria Giornaliero</span>
|
||||
@@ -141,7 +131,7 @@
|
||||
// Notification settings
|
||||
|
||||
// Notification settings
|
||||
document.getElementById('notifyWeeklyParking').checked = currentUser.notify_weekly_parking !== 0;
|
||||
|
||||
document.getElementById('notifyDailyParking').checked = currentUser.notify_daily_parking !== 0;
|
||||
document.getElementById('notifyDailyHour').value = currentUser.notify_daily_parking_hour || 8;
|
||||
document.getElementById('notifyDailyMinute').value = currentUser.notify_daily_parking_minute || 0;
|
||||
@@ -164,7 +154,7 @@
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
notify_weekly_parking: document.getElementById('notifyWeeklyParking').checked ? 1 : 0,
|
||||
notify_weekly_parking: 0,
|
||||
notify_daily_parking: document.getElementById('notifyDailyParking').checked ? 1 : 0,
|
||||
notify_daily_parking_hour: parseInt(document.getElementById('notifyDailyHour').value),
|
||||
notify_daily_parking_minute: parseInt(document.getElementById('notifyDailyMinute').value),
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="office-display-header"
|
||||
style="margin-bottom: 1rem; font-weight: 600; font-size: 1.1rem; color: var(--text);">
|
||||
Ufficio: <span id="currentOfficeNameDisplay" style="color: var(--primary);">Loading...</span>
|
||||
@@ -100,8 +101,36 @@
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-absent"></div>
|
||||
<span>Assente</span>
|
||||
<span>Ferie</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-business_trip"></div>
|
||||
<span>Trasferta</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Card (Admin/Manager only) -->
|
||||
<div id="exportCard" class="card" style="display: none;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 1rem; font-size: 1.1rem;">Esporta Report</h3>
|
||||
<div style="display: flex; gap: 1rem; align-items: flex-end;">
|
||||
<div>
|
||||
<small style="display: block; margin-bottom: 0.25rem;">Da:</small>
|
||||
<input type="date" id="exportStartDate" class="form-control">
|
||||
</div>
|
||||
<div>
|
||||
<small style="display: block; margin-bottom: 0.25rem;">A:</small>
|
||||
<input type="date" id="exportEndDate" class="form-control">
|
||||
</div>
|
||||
<button id="exportBtn" class="btn btn-dark" style="height: 38px;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
Esporta Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +156,11 @@
|
||||
</button>
|
||||
<button class="status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Assente</span>
|
||||
<span>Ferie</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="business_trip">
|
||||
<div class="status-icon status-business_trip"></div>
|
||||
<span>Trasferta</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-full" id="clearDayBtn" style="margin-top: 1rem;">Cancella
|
||||
|
||||
Reference in New Issue
Block a user