// Global state let token = localStorage.getItem('token'); let currentUser = null; let currentRecords = []; let filteredRecords = []; // Records after filtering let editingRecordId = null; let sortColumn = null; let sortDirection = 'asc'; // 'asc' or 'desc' let sortById = false; // Sort by ID toggle let allowDelete = localStorage.getItem('allowDelete') === 'true'; // Delete permission let searchFilters = { client: '', type: '', offer: '', work: '', byYear: false }; // API Base URL const API_BASE = ''; // Permission helpers function canEditProblems() { // Only user and admin can edit problems return currentUser && (currentUser.role === 'user' || currentUser.role === 'admin'); } function canEditRecords() { // Only admin can edit records (add/edit/delete) return currentUser && currentUser.role === 'admin'; } function canToggleDates() { // Admin and User can toggle dates (Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS) return currentUser && (currentUser.role === 'admin' || currentUser.role === 'user'); } function isGuest() { // Check if user is guest (read-only) return !currentUser || currentUser.role === 'guest'; } // Setup axios response interceptor to handle token refresh axios.interceptors.response.use( (response) => { // Check if server sent a refreshed token const refreshedToken = response.headers['x-refreshed-token']; if (refreshedToken) { // Update token in memory and localStorage token = refreshedToken; localStorage.setItem('token', refreshedToken); // Restart session timer with new token if (window.sessionTimerInterval) { startSessionTimer(); } } return response; }, (error) => { return Promise.reject(error); } ); // Field colors (fixed by field name) const FIELD_COLORS = { 'material': 'bg-white border border-gray-300 text-gray-900', // MATERJAL - valge taust 'package': 'bg-white border border-gray-300 text-gray-900', // PAKETT - valge taust 'worksheets': 'bg-white border border-gray-300 text-gray-900', // TÖÖLEHTI - valge taust (tühi) 'worksheets_filled': 'bg-green-500 text-white', // TÖÖLEHTI - roheline (kuupäevaga) 'cutting': 'bg-white border border-gray-300 text-gray-900', // LÕIKUS - valge (tühi) 'cutting_filled': 'bg-green-500 text-white', // LÕIKUS - roheline (kuupäevaga) 'glazing': 'bg-white border border-gray-300 text-gray-900', // KLAASIMINE - valge (tühi) 'glazing_filled': 'bg-green-500 text-white', // KLAASIMINE - roheline (kuupäevaga) 'ready': 'bg-white border border-gray-300 text-gray-900', // VALMIS - valge (tühi) 'ready_filled': 'bg-green-500 text-white', // VALMIS - roheline (kuupäevaga) 'issued': 'bg-white border border-gray-300 text-gray-900', // VÄLJASTATUD - valge (tühi) 'issued_filled': 'bg-green-500 text-white' // VÄLJASTATUD - roheline (kuupäevaga) }; // Initialize app document.addEventListener('DOMContentLoaded', async () => { // Check if user is logged in if (token) { currentUser = JSON.parse(localStorage.getItem('user')); // Check if session is still valid const expiry = getTokenExpiry(); if (expiry && Date.now() < expiry) { // Session still valid, start timer startSessionTimer(); } else { // Session expired, logout logout(); } } else { // Set default guest user (read-only access) currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' }; } // Show login modal for guest users, or main app for authenticated users if (currentUser.role === 'guest') { showLoginModal(); } else { showMainApp(); } await initFilters(); loadRecords(); document.getElementById('loginForm').addEventListener('submit', handleLogin); document.getElementById('recordForm').addEventListener('submit', handleSaveRecord); document.getElementById('monthFilter').addEventListener('change', loadRecords); document.getElementById('yearFilter').addEventListener('change', loadRecords); // Add listener for MAT-1 to control MAT-2 availability document.getElementById('materialDate').addEventListener('change', updateMat2State); document.getElementById('materialDate').addEventListener('input', updateMat2State); // Add listener for price field to auto-format (accept both comma and dot) const priceField = document.getElementById('price'); if (priceField) { priceField.addEventListener('input', function(e) { // Replace comma with dot automatically let value = e.target.value; if (value.includes(',')) { e.target.value = value.replace(',', '.'); } }); } // Add search filter listeners document.getElementById('searchClient').addEventListener('input', handleSearchFilter); document.getElementById('searchType').addEventListener('input', handleSearchFilter); document.getElementById('searchOffer').addEventListener('input', handleSearchFilter); document.getElementById('searchWork').addEventListener('input', handleSearchFilter); document.getElementById('searchByYear').addEventListener('change', handleSearchFilter); }); function openLoginModal() { document.getElementById('loginModal').classList.add('active'); document.getElementById('username').value = ''; document.getElementById('password').value = ''; document.getElementById('loginError').classList.add('hidden'); } function closeLoginModal() { document.getElementById('loginModal').classList.remove('active'); } // Make continueAsGuest globally accessible for onclick window.continueAsGuest = function() { // User chose to continue as guest (read-only mode) currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' }; closeLoginModal(); showMainApp(); loadRecords(); } function showLoginModal() { document.getElementById('mainApp').classList.add('hidden'); document.getElementById('loginModal').classList.add('active'); document.getElementById('username').value = ''; document.getElementById('password').value = ''; document.getElementById('loginError').classList.add('hidden'); } function showMainApp() { document.getElementById('mainApp').classList.remove('hidden'); document.getElementById('loginModal').classList.remove('active'); // Update UI based on role const role = currentUser?.role || 'guest'; // Show/hide elements based on role if (role === 'admin' || role === 'user') { document.getElementById('userInfo').classList.remove('hidden'); document.getElementById('userName').textContent = currentUser?.full_name || currentUser?.username || ''; document.getElementById('settingsBtn').classList.remove('hidden'); document.getElementById('logoutBtn').classList.remove('hidden'); document.getElementById('loginBtn').classList.add('hidden'); // Admin-specific UI if (role === 'admin') { document.body.classList.add('role-admin'); // Show "Lisa uus rida" button only for admins const addBtn = document.getElementById('addNewRowBtn'); if (addBtn) addBtn.classList.remove('hidden'); } else { document.body.classList.remove('role-admin'); // Hide "Lisa uus rida" button for users const addBtn = document.getElementById('addNewRowBtn'); if (addBtn) addBtn.classList.add('hidden'); } } else { // Guest user - read-only mode document.getElementById('userInfo').classList.add('hidden'); document.getElementById('settingsBtn').classList.add('hidden'); document.getElementById('logoutBtn').classList.add('hidden'); document.getElementById('loginBtn').classList.remove('hidden'); document.body.classList.remove('role-admin'); // Hide "Lisa uus rida" button for guests const addBtn = document.getElementById('addNewRowBtn'); if (addBtn) addBtn.classList.add('hidden'); } } async function handleLogin(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; const errorDiv = document.getElementById('loginError'); try { const response = await axios.post(`${API_BASE}/api/auth/login`, { username, password }); token = response.data.token; currentUser = response.data.user; localStorage.setItem('token', token); localStorage.setItem('user', JSON.stringify(currentUser)); closeLoginModal(); showMainApp(); loadRecords(); // Start session timer startSessionTimer(); } catch (error) { errorDiv.textContent = error.response?.data?.error || 'Vale kasutajanimi või parool'; errorDiv.classList.remove('hidden'); } } function logout() { // Stop session timer if (window.sessionTimerInterval) { clearInterval(window.sessionTimerInterval); window.sessionTimerInterval = null; } token = null; localStorage.removeItem('token'); localStorage.removeItem('user'); // Reset to guest user (read-only) currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' }; // Hide session timer document.getElementById('sessionTimer').classList.add('hidden'); // Show login modal for guest users showLoginModal(); } // Session management functions function getTokenExpiry() { if (!token) return null; try { const payload = JSON.parse(atob(token)); return payload.exp; } catch { return null; } } function checkSessionExpiry() { const expiry = getTokenExpiry(); if (!expiry) return; const now = Date.now(); const timeLeft = expiry - now; // If session expired, logout if (timeLeft <= 0) { alert('Sessioon on aegunud. Palun logige uuesti sisse.'); logout(); return; } // Show timer only in last minute (60 seconds) const sessionTimerEl = document.getElementById('sessionTimer'); const sessionTimeLeftEl = document.getElementById('sessionTimeLeft'); if (timeLeft <= 60000) { // 60 seconds = 60000 ms const secondsLeft = Math.ceil(timeLeft / 1000); sessionTimerEl.classList.remove('hidden'); sessionTimeLeftEl.textContent = `${secondsLeft}s`; } else { sessionTimerEl.classList.add('hidden'); } } function startSessionTimer() { // Clear any existing timer if (window.sessionTimerInterval) { clearInterval(window.sessionTimerInterval); } // Check immediately checkSessionExpiry(); // Check every second window.sessionTimerInterval = setInterval(checkSessionExpiry, 1000); } async function loadYears() { try { const response = await axios.get(`${API_BASE}/api/years`); const { years } = response.data; const currentYear = new Date().getFullYear(); // Populate main year filter const yearFilter = document.getElementById('yearFilter'); yearFilter.innerHTML = ''; years.forEach(year => { const option = document.createElement('option'); option.value = year; option.textContent = year; if (year === currentYear) { option.selected = true; } yearFilter.appendChild(option); }); // Populate report year filter const reportYear = document.getElementById('reportYear'); if (reportYear) { reportYear.innerHTML = ''; years.forEach(year => { const option = document.createElement('option'); option.value = year; option.textContent = year; if (year === currentYear) { option.selected = true; } reportYear.appendChild(option); }); } } catch (error) { console.error('Load years error:', error); // Fallback: use current year if API fails const currentYear = new Date().getFullYear(); const yearFilter = document.getElementById('yearFilter'); yearFilter.innerHTML = ``; } } async function initFilters() { // Set to January (month 1) by default since that's where demo data exists document.getElementById('monthFilter').value = 1; // Load years dynamically await loadYears(); // Default checkbox unchecked (search by current month) document.getElementById('searchByYear').checked = false; } async function loadRecords() { const year = document.getElementById('yearFilter').value; const byYear = document.getElementById('searchByYear').checked; try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; // Load all 12 months ONLY if byYear checkbox is checked if (byYear) { const promises = []; for (let month = 1; month <= 12; month++) { promises.push( axios.get(`${API_BASE}/api/records`, { params: { month, year }, headers }) ); } const responses = await Promise.all(promises); currentRecords = responses.flatMap(response => response.data); } else { // Load only current month const month = document.getElementById('monthFilter').value; const response = await axios.get(`${API_BASE}/api/records`, { params: { month, year }, headers }); currentRecords = response.data; } applyFilters(); // Apply search filters after loading } catch (error) { console.error('Load records error:', error); } } // Sort records by column // Toggle sort by ID function toggleSortById() { // Toggle sort direction if (sortById && sortDirection === 'asc') { sortDirection = 'desc'; } else if (sortById && sortDirection === 'desc') { sortById = false; sortDirection = 'asc'; } else { sortById = true; sortDirection = 'asc'; } // Update icon const icon = document.getElementById('sortByIdIcon'); if (!sortById) { icon.className = 'fas fa-sort text-gray-400'; } else if (sortDirection === 'asc') { icon.className = 'fas fa-sort-up text-indigo-600'; } else { icon.className = 'fas fa-sort-down text-indigo-600'; } // Reset column sort when sorting by ID if (sortById) { sortColumn = null; } // Apply sort and render if (sortById) { // Sort by ID filteredRecords.sort((a, b) => { if (sortDirection === 'asc') { return a.id - b.id; } else { return b.id - a.id; } }); } renderRecords(); } function sortRecords(column) { // Disable ID sort when sorting by column sortById = false; document.getElementById('sortByIdIcon').className = 'fas fa-sort text-gray-400'; // Toggle direction if clicking same column, otherwise reset to asc if (sortColumn === column) { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; } else { sortColumn = column; sortDirection = 'asc'; } // Sort the filteredRecords array (not currentRecords) filteredRecords.sort((a, b) => { let aVal, bVal; switch(column) { case 'client': aVal = (a.client_name || '').toLowerCase(); bVal = (b.client_name || '').toLowerCase(); break; case 'type': aVal = (a.type || '').toLowerCase(); bVal = (b.type || '').toLowerCase(); break; case 'offer': aVal = (a.offer_number || '').toLowerCase(); bVal = (b.offer_number || '').toLowerCase(); break; case 'work': aVal = (a.work_number || '').toLowerCase(); bVal = (b.work_number || '').toLowerCase(); break; case 'material': aVal = a.material_date || ''; bVal = b.material_date || ''; break; case 'package': aVal = a.package_date || ''; bVal = b.package_date || ''; break; default: return 0; } if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; return 0; }); // Update sort icons updateSortIcons(column, sortDirection); renderRecords(); } // Update sort icons in table headers function updateSortIcons(activeColumn, direction) { const columns = ['client', 'type', 'offer', 'work', 'material', 'package']; columns.forEach(col => { const icon = document.getElementById(`sort-icon-${col}`); if (!icon) return; if (col === activeColumn) { // Active column - show direction icon.className = `fas fa-sort-${direction === 'asc' ? 'up' : 'down'} text-indigo-600 ml-2`; } else { // Inactive column - show neutral sort icon icon.className = 'fas fa-sort text-gray-400 ml-2'; } }); } // Handle search filter input async function handleSearchFilter() { searchFilters.client = document.getElementById('searchClient').value.toLowerCase().trim(); searchFilters.type = document.getElementById('searchType').value.toLowerCase().trim(); searchFilters.offer = document.getElementById('searchOffer').value.toLowerCase().trim(); searchFilters.work = document.getElementById('searchWork').value.toLowerCase().trim(); searchFilters.byYear = document.getElementById('searchByYear').checked; // Reload records from server when filters change // This ensures we search across all data, not just currently loaded records await loadRecords(); } // Apply search filters to currentRecords function applyFilters() { // Start with all records filteredRecords = currentRecords.filter(record => { // Text filters if (searchFilters.client && !(record.client_name || '').toLowerCase().includes(searchFilters.client)) { return false; } if (searchFilters.type && !(record.type || '').toLowerCase().includes(searchFilters.type)) { return false; } if (searchFilters.offer && !(record.offer_number || '').toLowerCase().includes(searchFilters.offer)) { return false; } if (searchFilters.work && !(record.work_number || '').toLowerCase().includes(searchFilters.work)) { return false; } // Year filter - if checked, filter by current year instead of current month if (searchFilters.byYear) { // Get current year from yearFilter const currentYear = parseInt(document.getElementById('yearFilter').value); // Check if record's year matches if (record.year !== currentYear) { return false; } // Note: We don't filter by month when byYear is true } return true; }); // Re-apply sorting if active if (sortColumn) { sortRecords(sortColumn); } else { renderRecords(); } } // Clear all filters function clearAllFilters() { // Clear text search filters document.getElementById('searchClient').value = ''; document.getElementById('searchType').value = ''; document.getElementById('searchOffer').value = ''; document.getElementById('searchWork').value = ''; // Uncheck year filter document.getElementById('searchByYear').checked = false; // Reset search filters object searchFilters = { client: '', type: '', offer: '', work: '', byYear: false }; // Reset sorting to default (ID ASC from backend) sortColumn = null; sortDirection = 'asc'; // Clear all sort icons const sortIcons = document.querySelectorAll('[id^="sort-icon-"]'); sortIcons.forEach(icon => { icon.className = 'fas fa-sort text-gray-300 ml-1 text-xs'; }); // Reapply filters (which now are empty, showing all records) applyFilters(); } function renderRecords() { const tbody = document.getElementById('recordsTable'); if (filteredRecords.length === 0) { tbody.innerHTML = `

Kirjeid ei leitud

`; // Show footer with zero totals const tfoot = document.getElementById('recordsTableFooter'); tfoot.innerHTML = ` Summa: 0 0.00 `; return; } // All users can edit dates (removed admin-only check in v4.0.8) tbody.innerHTML = filteredRecords.map(record => { // Check if record has error flags (blocks ready and issued) // Note: problems text is just a comment, not a blocker const hasProblems = record.worksheets_error === 1 || record.cutting_error === 1 || record.glazing_error === 1 || record.ready_error === 1 || record.issued_error === 1; return ` ${record.client_name || ''} ${record.type || '-'} ${record.offer_number || '-'} ${record.work_number || '-'} ${record.quantity || 0} ${record.color || '-'} ${renderCalendarCell(record.id, 'material', record.material_date, null, record.material_confirmed)} ${renderCalendarCell(record.id, 'material2', record.material2_date, record.material_date, record.material2_confirmed)} ${renderCalendarCell(record.id, 'package', record.package_date, null)} ${renderWorksheetsCell(record.id, record.worksheets_date, record.worksheets_confirmed, record.worksheets_error, record.problems || '')} ${renderDateCell(record.id, 'cutting', record.cutting_date, record.cutting_error, false, record.problems || '')} ${renderDateCell(record.id, 'glazing', record.glazing_date, record.glazing_error, false, record.problems || '')} ${renderDateCell(record.id, 'ready', record.ready_date, record.ready_error, hasProblems, record.problems || '')} ${renderDateCell(record.id, 'issued', record.issued_date, record.issued_error, hasProblems, record.problems || '')} ${renderNotesCell(record.id, record.notes, record.notes_date)} ${renderProblemsCell(record.id, record.problems, record.problems_date, record)} ${record.installer || '-'} ${renderPriceCell(record.id, record.price, record.price_paid, record.arve_checked, record.arve_makstud)} `; }).join(''); // Calculate totals from filtered records const totalQuantity = filteredRecords.reduce((sum, record) => sum + (parseInt(record.quantity) || 0), 0); const totalPrice = filteredRecords.reduce((sum, record) => sum + (parseFloat(record.price) || 0), 0); // Render footer with totals const tfoot = document.getElementById('recordsTableFooter'); tfoot.innerHTML = ` Summa: ${totalQuantity} ${totalPrice.toFixed(2)} `; // Toggle delete buttons based on permission toggleDeleteButtons(); } // Render read-only cell for users (MAT/PAK without click) function renderReadOnlyCell(date) { if (!date) { return `
-
`; } // Format date as DD.MM.YYYY const dateObj = new Date(date + 'T00:00:00'); const day = String(dateObj.getDate()).padStart(2, '0'); const month = String(dateObj.getMonth() + 1).padStart(2, '0'); const year = dateObj.getFullYear(); const formattedDate = `${day}.${month}.${year}`; return `
${formattedDate}
`; } // Render calendar cell for MAT/PAK (white background, clickable to open calendar) function renderCalendarCell(recordId, field, date, materialDate = null, materialConfirmed = 0) { const fieldId = `${field}_${recordId}`; // Check if material2 should be blocked (when material is empty) const isBlocked = field === 'material2' && !materialDate; // Determine border color (green if confirmed, gray otherwise) - for material or material2 field const isConfirmed = (field === 'material' || field === 'material2') && materialConfirmed === 1; const borderColor = isConfirmed ? 'border-green-500' : 'border-gray-300'; if (!date) { // Empty cell - click to open calendar (or show blocked) if (isBlocked) { return `
`; } return ` `; } // Format date as DD.MM.YYYY const dateObj = new Date(date + 'T00:00:00'); const day = String(dateObj.getDate()).padStart(2, '0'); const month = String(dateObj.getMonth() + 1).padStart(2, '0'); const year = dateObj.getFullYear(); const formattedDate = `${day}.${month}.${year}`; // Blocked cell with date (should not happen, but handle it) if (isBlocked) { return `
${formattedDate}
`; } // For material field (MAT-1) or material2 field (MAT-2), show confirm button to the right if (field === 'material' || field === 'material2') { const toggleFunction = field === 'material' ? 'toggleMaterialConfirmed' : 'toggleMaterial2Confirmed'; const titleConfirmed = field === 'material' ? 'Materjal kinnitatud' : 'Materjal 2 kinnitatud'; const titleNotConfirmed = field === 'material' ? 'Kinnita materjali kättesaamine' : 'Kinnita materjali 2 kättesaamine'; return `
`; } return ` `; } // Update date from calendar picker async function updateDateFromCalendar(recordId, field, newDate) { // Allow empty string to clear the date try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; await axios.patch( `${API_BASE}/api/records/${recordId}/status`, { field, date: null, newDate: newDate || null }, { headers } ); await loadRecords(); } catch (error) { console.error('Update date from calendar error:', error); // Error removed - operation works correctly for both admin and public users } } // Render date cell with fixed color (color determined by field name) function renderDateCell(recordId, field, date, hasError, isBlocked, problemText = '') { // Check if error flag is set const isError = hasError === 1; // Prepare tooltip text const tooltipText = (isError || isBlocked) && problemText ? problemText.replace(/"/g, '"').replace(/'/g, ''').replace(/\n/g, ' ') : ''; // If field is blocked, show as disabled but clickable (only lock icon, no date) if (isBlocked) { const blockedColorClass = 'bg-gray-300 text-gray-600 border border-gray-400 cursor-pointer hover:bg-gray-400'; // Escape problem text for onclick const escapedProblemText = problemText.replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); return `
`; } if (!date) { // Empty cell - light red for all errors, white if normal let emptyColorClass; if (isError) { // Light red for all error flags emptyColorClass = 'bg-red-100 border border-red-300 text-red-800'; } else { emptyColorClass = FIELD_COLORS[field] || 'bg-white border border-gray-300 text-gray-400'; } return `
-
`; } // Format date as DD.MM.YYYY const formattedDate = formatDate(date); // Filled cell - light red for all errors, green if normal let filledColorClass; if (isError) { // Light red for all error flags filledColorClass = 'bg-red-100 border border-red-300 text-red-800'; } else { filledColorClass = FIELD_COLORS[field + '_filled'] || 'bg-green-500 text-white'; } return `
${formattedDate}
`; } // Helper function to format date function formatDate(date) { const dateObj = new Date(date + 'T00:00:00'); const day = String(dateObj.getDate()).padStart(2, '0'); const month = String(dateObj.getMonth() + 1).padStart(2, '0'); const year = dateObj.getFullYear(); return `${day}.${month}.${year}`; } // Render worksheets cell with 3-step cycle logic function renderWorksheetsCell(recordId, date, confirmed, hasError, problemText = '') { const isError = hasError === 1; // Prepare tooltip text const tooltipText = isError && problemText ? problemText.replace(/"/g, '"').replace(/'/g, ''').replace(/\n/g, ' ') : ''; // Step 1: No date (empty cell) if (!date) { const emptyColorClass = isError ? 'bg-red-100 border border-red-300 text-red-800' : 'bg-white border border-gray-300 text-gray-400'; const defaultTitle = "Klõps 1: Lisa kuupäev"; return `
-
`; } // Format date const formattedDate = formatDate(date); // Step 2: Has date, not confirmed (gray) if (confirmed === 0) { const grayColorClass = isError ? 'bg-red-100 border border-red-300 text-red-800' : 'bg-gray-200 border border-gray-400 text-gray-900'; const defaultTitle = "Klõps 2: Kinnita"; return `
${formattedDate}
`; } // Step 3: Has date, confirmed (green) const greenColorClass = isError ? 'bg-red-100 border border-red-300 text-red-800' : 'bg-green-500 border border-green-600 text-white'; const defaultTitle = "Klõps 3: Tühjenda"; return `
${formattedDate}
`; } function renderNotesCell(recordId, notes, notesDate) { // Prepare tooltip text - escape for HTML attribute const tooltipText = notes ? notes.replace(/"/g, '"').replace(/'/g, ''').replace(/\n/g, ' ') : ''; // If has notes text - show YELLOW with info icon if (notes && notes.trim()) { return `
`; } // No notes - show empty cell with click to add return `
-
`; } function renderProblemsCell(recordId, problems, problemsDate, record) { // Prepare error flags object for passing to openProblemsModal const errorFlags = { worksheets_error: record.worksheets_error || 0, cutting_error: record.cutting_error || 0, glazing_error: record.glazing_error || 0, ready_error: record.ready_error || 0, issued_error: record.issued_error || 0 }; const errorFlagsJson = JSON.stringify(errorFlags).replace(/"/g, '"'); // Check if has error flags (checkboxes) const hasProblems = record.worksheets_error === 1 || record.cutting_error === 1 || record.glazing_error === 1 || record.ready_error === 1 || record.issued_error === 1; // Prepare tooltip text from problems text field const tooltipText = problems ? problems.replace(/"/g, '"').replace(/'/g, ''').replace(/\n/g, ' ') : ''; // If has error flags (checkboxes checked) - show RED with exclamation mark if (hasProblems) { return `
`; } // If has problems text but no error flags - show gray with icon if (problems && problems.trim()) { return `
`; } // No problems - show empty cell with click to add return `
-
`; } // Toggle date: if has date - remove, if no date - add current date async function toggleDate(recordId, field, currentDate) { // Check permissions - admin and user can toggle dates if (!canToggleDates()) { alert('Sul pole õigust andmeid muuta. Palun logi sisse.'); return; } try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; const response = await axios.patch( `${API_BASE}/api/records/${recordId}/status`, { field, date: currentDate }, { headers } ); if (response.data.success) { await loadRecords(); // Refresh table } } catch (error) { console.error('Toggle date error:', error); // Check if field is blocked by problems if (error.response?.status === 403 && error.response?.data?.error === 'blocked') { openBlockedFieldModal(error.response.data.message); } else if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logi uuesti sisse.'); logout(); } } } // Update MAT-2 state based on MAT-1 function updateMat2State() { const mat1 = document.getElementById('materialDate'); const mat2 = document.getElementById('material2Date'); if (!mat1.value) { mat2.disabled = true; mat2.value = ''; mat2.classList.add('bg-gray-100', 'cursor-not-allowed'); } else { mat2.disabled = false; mat2.classList.remove('bg-gray-100', 'cursor-not-allowed'); } } function openModal() { // Check if user is authenticated if (!token || !currentUser || currentUser.role !== 'admin') { alert('Ainult administraator saab lisada uusi kirjeid. Palun logi sisse.'); openLoginModal(); return; } editingRecordId = null; document.getElementById('modalTitle').textContent = 'Lisa uus kirje'; document.getElementById('recordForm').reset(); document.getElementById('recordModal').classList.add('active'); updateMat2State(); // Check MAT-2 state on open } function closeModal() { document.getElementById('recordModal').classList.remove('active'); editingRecordId = null; } function editRecord(recordId) { // Check if user is authenticated if (!token || !currentUser || currentUser.role !== 'admin') { alert('Ainult administraator saab muuta kirjeid. Palun logi sisse.'); openLoginModal(); return; } const record = currentRecords.find(r => r.id === recordId); if (!record) return; editingRecordId = recordId; document.getElementById('modalTitle').textContent = 'Muuda kirjet'; document.getElementById('recordId').value = record.id; document.getElementById('clientName').value = record.client_name || ''; document.getElementById('type').value = record.type || ''; document.getElementById('offerNumber').value = record.offer_number || ''; document.getElementById('workNumber').value = record.work_number || ''; document.getElementById('quantity').value = record.quantity || ''; document.getElementById('color').value = record.color || ''; document.getElementById('notes').value = record.notes || ''; document.getElementById('installer').value = record.installer || ''; document.getElementById('price').value = record.price || ''; document.getElementById('arveChecked').checked = record.arve_checked === 1; document.getElementById('arveMakstud').value = record.arve_makstud || ''; document.getElementById('materialDate').value = record.material_date || ''; document.getElementById('material2Date').value = record.material2_date || ''; document.getElementById('packageDate').value = record.package_date || ''; document.getElementById('recordModal').classList.add('active'); updateMat2State(); // Check MAT-2 state after loading data } async function handleSaveRecord(e) { e.preventDefault(); const data = { month: parseInt(document.getElementById('monthFilter').value), year: parseInt(document.getElementById('yearFilter').value), client_name: document.getElementById('clientName').value, type: document.getElementById('type').value || null, offer_number: document.getElementById('offerNumber').value || null, work_number: document.getElementById('workNumber').value || null, quantity: parseInt(document.getElementById('quantity').value), color: document.getElementById('color').value || null, notes: document.getElementById('notes').value || null, installer: document.getElementById('installer').value || null, price: parseFloat(document.getElementById('price').value.replace(',', '.')) || 0, arve_checked: document.getElementById('arveChecked').checked ? 1 : 0, arve_makstud: document.getElementById('arveMakstud').value || null, // Only include date fields that are actually in the form // Töölehti, LÕI, KLA, VAL, VÄL are managed separately via toggle dates material_date: document.getElementById('materialDate').value || null, material2_date: document.getElementById('material2Date').value || null, package_date: document.getElementById('packageDate').value || null }; try { if (editingRecordId) { await axios.put( `${API_BASE}/api/records/${editingRecordId}`, data, { headers: { Authorization: `Bearer ${token}` } } ); } else { await axios.post( `${API_BASE}/api/records`, data, { headers: { Authorization: `Bearer ${token}` } } ); } await loadRecords(); // Reload data first closeModal(); // Then close modal } catch (error) { console.error('Save record error:', error); // More detailed error handling if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logi uuesti sisse.'); // Clear invalid token localStorage.removeItem('token'); localStorage.removeItem('user'); token = null; currentUser = null; location.reload(); } else if (error.response?.status === 400) { alert('Viga: ' + (error.response?.data?.error || 'Kontrolli väljad')); } else { alert('Viga salvestamisel: ' + (error.response?.data?.error || error.message)); } } } // Blocked field modal functions function openBlockedFieldModal(problemText) { document.getElementById('blockedFieldMessage').textContent = problemText; document.getElementById('blockedFieldModal').classList.add('active'); } function closeBlockedFieldModal() { document.getElementById('blockedFieldModal').classList.remove('active'); document.getElementById('blockedFieldMessage').textContent = ''; } // Notes modal functions function openNotesModal(recordId, notes) { document.getElementById('notesRecordId').value = recordId; document.getElementById('notesText').value = notes.replace(/\\n/g, '\n').replace(/\\'/g, "'"); // Disable inputs for non-admin users (only admin can edit notes) const readOnly = !canEditRecords(); document.getElementById('notesText').readOnly = readOnly; // Hide save button for non-admins const saveBtn = document.querySelector('#notesModal button[type="submit"]'); if (saveBtn) { saveBtn.style.display = readOnly ? 'none' : 'inline-block'; } document.getElementById('notesModal').classList.add('active'); } function closeNotesModal() { document.getElementById('notesModal').classList.remove('active'); document.getElementById('notesRecordId').value = ''; document.getElementById('notesText').value = ''; } async function saveNotes(event) { event.preventDefault(); // Check permissions - only admin if (!canEditRecords()) { alert('Sul pole õigust märkmeid muuta. Palun logi sisse administraatorina.'); return; } const recordId = document.getElementById('notesRecordId').value; const notes = document.getElementById('notesText').value; try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; await axios.patch( `${API_BASE}/api/records/${recordId}/notes`, { notes }, { headers } ); await loadRecords(); // Reload data first closeNotesModal(); // Then close modal } catch (error) { console.error('Save notes error:', error); alert('Viga märkuste salvestamisel'); } } // Problems modal functions function openProblemsModal(recordId, problems, errorFlags = {}) { document.getElementById('problemsRecordId').value = recordId; document.getElementById('problemsText').value = problems.replace(/\\n/g, '\n').replace(/\\'/g, "'"); // Set error checkboxes based on current error flags document.getElementById('errorWorksheets').checked = errorFlags.worksheets_error === 1; document.getElementById('errorCutting').checked = errorFlags.cutting_error === 1; document.getElementById('errorGlazing').checked = errorFlags.glazing_error === 1; document.getElementById('errorReady').checked = errorFlags.ready_error === 1; document.getElementById('errorIssued').checked = errorFlags.issued_error === 1; // Disable inputs for guest users const readOnly = !canEditProblems(); document.getElementById('problemsText').readOnly = readOnly; document.getElementById('errorWorksheets').disabled = readOnly; document.getElementById('errorCutting').disabled = readOnly; document.getElementById('errorGlazing').disabled = readOnly; document.getElementById('errorReady').disabled = readOnly; document.getElementById('errorIssued').disabled = readOnly; // Hide save button for guests const saveBtn = document.querySelector('#problemsModal button[type="submit"]'); if (saveBtn) { saveBtn.style.display = readOnly ? 'none' : 'inline-block'; } document.getElementById('problemsModal').classList.add('active'); } function closeProblemsModal() { document.getElementById('problemsModal').classList.remove('active'); document.getElementById('problemsRecordId').value = ''; document.getElementById('problemsText').value = ''; // Clear error checkboxes document.getElementById('errorWorksheets').checked = false; document.getElementById('errorCutting').checked = false; document.getElementById('errorGlazing').checked = false; document.getElementById('errorReady').checked = false; document.getElementById('errorIssued').checked = false; } async function saveProblems(event) { event.preventDefault(); // Check permissions if (!canEditProblems()) { alert('Sul pole õigust probleeme muuta. Palun logi sisse.'); return; } const recordId = document.getElementById('problemsRecordId').value; const problems = document.getElementById('problemsText').value; // Get error flags from checkboxes const errorFlags = { worksheets: document.getElementById('errorWorksheets').checked, cutting: document.getElementById('errorCutting').checked, glazing: document.getElementById('errorGlazing').checked, ready: document.getElementById('errorReady').checked, issued: document.getElementById('errorIssued').checked }; try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; await axios.patch( `${API_BASE}/api/records/${recordId}/problems`, { problems, errorFlags }, { headers } ); await loadRecords(); // Reload data first closeProblemsModal(); // Then close modal } catch (error) { console.error('Save problems error:', error); if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logi uuesti sisse.'); logout(); } else { alert('Viga probleemide salvestamisel'); } } } // Settings modal functions function openSettingsModal() { document.getElementById('settingsUsername').value = currentUser.username; document.getElementById('settingsFullName').value = currentUser.full_name || currentUser.username; document.getElementById('settingsCurrentPassword').value = ''; document.getElementById('settingsNewPassword').value = ''; document.getElementById('settingsConfirmPassword').value = ''; document.getElementById('allowDeleteCheckbox').checked = allowDelete; document.getElementById('settingsError').classList.add('hidden'); document.getElementById('settingsSuccess').classList.add('hidden'); document.getElementById('settingsModal').classList.add('active'); } function closeSettingsModal() { document.getElementById('settingsModal').classList.remove('active'); } document.getElementById('settingsForm').addEventListener('submit', async function(event) { event.preventDefault(); const fullName = document.getElementById('settingsFullName').value; const currentPassword = document.getElementById('settingsCurrentPassword').value; const newPassword = document.getElementById('settingsNewPassword').value; const confirmPassword = document.getElementById('settingsConfirmPassword').value; const errorDiv = document.getElementById('settingsError'); const successDiv = document.getElementById('settingsSuccess'); // Hide previous messages errorDiv.classList.add('hidden'); successDiv.classList.add('hidden'); // Validation if (!fullName.trim()) { errorDiv.textContent = 'Nimi ei saa olla tühi'; errorDiv.classList.remove('hidden'); return; } // If changing password, current password is required if (newPassword && !currentPassword) { errorDiv.textContent = 'Praegune parool on kohustuslik parooli muutmiseks'; errorDiv.classList.remove('hidden'); return; } // Check if passwords match (only if new password is provided) if (newPassword && newPassword !== confirmPassword) { errorDiv.textContent = 'Uued paroolid ei kattu'; errorDiv.classList.remove('hidden'); return; } try { const response = await axios.patch( `${API_BASE}/api/users/profile`, { full_name: fullName, current_password: currentPassword, new_password: newPassword || null }, { headers: { Authorization: `Bearer ${token}` } } ); // Update current user data currentUser.full_name = response.data.user.full_name; localStorage.setItem('user', JSON.stringify(currentUser)); // Update UI document.getElementById('userName').textContent = currentUser.full_name; // Show success message successDiv.textContent = 'Seaded edukalt salvestatud!'; successDiv.classList.remove('hidden'); // Close modal after 1.5 seconds setTimeout(() => { closeSettingsModal(); }, 1500); } catch (error) { console.error('Update settings error:', error); errorDiv.textContent = error.response?.data?.error || 'Viga seadete uuendamisel'; errorDiv.classList.remove('hidden'); } }); // Render price cell with green border when paid function renderPriceCell(recordId, price, pricePaid = 0, arveChecked = 0, arveMakstud = '') { if (!price) { return `-`; } // Use arve_checked for border styling (instead of price_paid) const isArveChecked = arveChecked === 1; const borderClass = isArveChecked ? 'border border-green-500' : ''; const bgClass = isArveChecked ? 'bg-green-50' : ''; // Prepare tooltip text from arve_makstud field const tooltipText = arveMakstud ? arveMakstud.replace(/"/g, '"').replace(/'/g, ''').replace(/\n/g, ' ') : (isArveChecked ? 'Arve' : 'Arve puudub'); return `
${price.toFixed(2)}
`; } // Toggle price paid status async function togglePricePaid(recordId) { // Check permissions - only admin if (!canEditRecords()) { alert('Sul pole õigust maksestaatust muuta. Palun logi sisse administraatorina.'); return; } try { const response = await axios.patch( `${API_BASE}/api/records/${recordId}/price-paid`, {}, { headers: { Authorization: `Bearer ${token}` } } ); if (response.data.success) { await loadRecords(); // Refresh table } } catch (error) { console.error('Toggle price paid error:', error); // Handle specific error cases if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logige uuesti sisse.'); // Clear invalid token localStorage.removeItem('token'); localStorage.removeItem('user'); token = null; currentUser = null; location.reload(); } else { alert('Viga maksestaatuse muutmisel: ' + (error.response?.data?.error || error.message)); } } } // Toggle material confirmed status async function toggleMaterialConfirmed(recordId) { // Check permissions - only admin if (!canEditRecords()) { alert('Sul pole õigust kinnitust muuta. Palun logi sisse administraatorina.'); return; } try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; const response = await axios.patch( `${API_BASE}/api/records/${recordId}/material-confirmed`, {}, { headers } ); if (response.data.success) { await loadRecords(); // Refresh table } } catch (error) { console.error('Toggle material confirmed error:', error); // Handle specific error cases if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logige uuesti sisse.'); // Clear invalid token localStorage.removeItem('token'); localStorage.removeItem('user'); token = null; currentUser = null; location.reload(); } else { alert('Viga materjali kinnitamisel: ' + (error.response?.data?.error || error.message)); } } } // Toggle material2_confirmed async function toggleMaterial2Confirmed(recordId) { // Check permissions - only admin if (!canEditRecords()) { alert('Sul pole õigust kinnitust muuta. Palun logi sisse administraatorina.'); return; } try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; const response = await axios.patch( `${API_BASE}/api/records/${recordId}/material2-confirmed`, {}, { headers } ); if (response.data.success) { await loadRecords(); // Refresh table } } catch (error) { console.error('Toggle material2 confirmed error:', error); // Handle specific error cases if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logige uuesti sisse.'); // Clear invalid token localStorage.removeItem('token'); localStorage.removeItem('user'); token = null; currentUser = null; location.reload(); } else { alert('Viga materjali 2 kinnitamisel: ' + (error.response?.data?.error || error.message)); } } } // Toggle worksheets with 3-step cycle async function toggleWorksheetsStep(recordId) { // Check permissions - admin and user can toggle if (!canToggleDates()) { alert('Sul pole õigust töölehe staatust muuta. Palun logi sisse.'); return; } try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; const response = await axios.patch( `${API_BASE}/api/records/${recordId}/worksheets-cycle`, {}, { headers } ); if (response.data.success) { await loadRecords(); // Refresh table } } catch (error) { console.error('Toggle worksheets step error:', error); alert('Viga Töölehti staatuse muutmisel'); } } // ==================== REPORT FUNCTIONALITY ==================== let reportData = []; let accountantReportData = []; // For accountant report let reportYear = 2025; let reportMonth = 1; let reportType = 'master'; // 'master' or 'accountant' let currentReportStep = 0; const MONTH_NAMES = [ 'Jaanuar', 'Veebruar', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', 'August', 'September', 'Oktoober', 'November', 'Detsember' ]; async function openReportModal() { document.getElementById('reportModal').classList.add('active'); currentReportStep = 0; reportType = 'master'; const currentDate = new Date(); reportYear = currentDate.getFullYear(); reportMonth = currentDate.getMonth() + 1; // Load years dynamically for report await loadYears(); showReportStep(0); } function closeReportModal() { document.getElementById('reportModal').classList.remove('active'); reportData = []; } function showReportStep(step) { // Hide all steps document.getElementById('reportStep0').classList.add('hidden'); document.getElementById('reportStep1').classList.add('hidden'); document.getElementById('reportStep2').classList.add('hidden'); document.getElementById('reportStep3').classList.add('hidden'); // Show current step document.getElementById(`reportStep${step}`).classList.remove('hidden'); // Update step indicators (0-3) for (let i = 0; i <= 3; i++) { const indicator = document.getElementById(`step${i}-indicator`); const circle = indicator.querySelector('div'); const text = indicator.querySelector('span'); if (i < step) { // Completed step circle.className = 'w-10 h-10 rounded-full bg-green-500 text-white flex items-center justify-center font-bold'; circle.innerHTML = ''; text.className = 'ml-2 text-sm font-medium text-green-500'; } else if (i === step) { // Current step circle.className = 'w-10 h-10 rounded-full bg-indigo-600 text-white flex items-center justify-center font-bold'; circle.textContent = i + 1; text.className = 'ml-2 text-sm font-medium text-indigo-600'; } else { // Future step circle.className = 'w-10 h-10 rounded-full bg-gray-300 text-gray-600 flex items-center justify-center font-bold'; circle.textContent = i + 1; text.className = 'ml-2 text-sm font-medium text-gray-600'; } } currentReportStep = step; } // Select report type (master or accountant) function selectReportType(type) { reportType = type; showReportStep(1); // Update step 1 content based on type const step1Title = document.getElementById('reportStep1Title'); const monthSelector = document.getElementById('reportMonthSelector'); if (type === 'master') { step1Title.textContent = 'Vali aruande aasta'; monthSelector.classList.add('hidden'); } else { step1Title.textContent = 'Vali periood'; monthSelector.classList.remove('hidden'); // Set current month document.getElementById('reportMonth').value = reportMonth; } } function goBackToReportStep0() { showReportStep(0); } function goToReportStep1() { showReportStep(1); } async function goToReportStep2() { reportYear = parseInt(document.getElementById('reportYear').value); if (reportType === 'master') { await loadReportData(); document.getElementById('masterReportTable').classList.remove('hidden'); document.getElementById('accountantReportTable').classList.add('hidden'); } else { reportMonth = parseInt(document.getElementById('reportMonth').value); await loadAccountantReportData(); document.getElementById('masterReportTable').classList.add('hidden'); document.getElementById('accountantReportTable').classList.remove('hidden'); } showReportStep(2); } function goToReportStep3() { generateReportData(); showReportStep(3); } async function loadReportData() { try { // Initialize report data structure for 12 months reportData = MONTH_NAMES.map((name, index) => ({ month: index + 1, monthName: name, workDays: '', windows: 0, price: 0, avgPerDay: 0 })); // Load saved work days from localStorage for this year const savedWorkDays = JSON.parse(localStorage.getItem(`workDays_${reportYear}`) || '{}'); // Fetch data for each month const headers = token ? { Authorization: `Bearer ${token}` } : {}; const promises = []; for (let month = 1; month <= 12; month++) { promises.push( axios.get(`${API_BASE}/api/records`, { params: { month, year: reportYear }, headers }) ); } const responses = await Promise.all(promises); // Process each month's data responses.forEach((response, index) => { const records = response.data; let totalWindows = 0; let totalPrice = 0; records.forEach(record => { totalWindows += parseInt(record.quantity) || 0; totalPrice += parseFloat(record.price) || 0; }); reportData[index].windows = totalWindows; reportData[index].price = totalPrice; // Restore saved work days for this month if (savedWorkDays[index + 1]) { reportData[index].workDays = savedWorkDays[index + 1]; } }); renderReportTable(); } catch (error) { console.error('Load report data error:', error); alert('Viga aruande andmete laadimisel'); } } function renderReportTable() { const tbody = document.getElementById('reportTableBody'); tbody.innerHTML = ''; reportData.forEach((monthData, index) => { const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-50'; // Calculate average per day if work days are filled let avgPerDay = '-'; if (monthData.workDays && parseInt(monthData.workDays) > 0) { avgPerDay = (monthData.windows / parseInt(monthData.workDays)).toFixed(2); monthData.avgPerDay = parseFloat(avgPerDay); } tr.innerHTML = ` ${monthData.monthName} ${monthData.windows} ${monthData.price.toFixed(2)} ${avgPerDay} `; tbody.appendChild(tr); }); updateReportTotals(); } function updateWorkDays(index, value) { reportData[index].workDays = value; // Save work days to localStorage const savedWorkDays = JSON.parse(localStorage.getItem(`workDays_${reportYear}`) || '{}'); savedWorkDays[reportData[index].month] = value; localStorage.setItem(`workDays_${reportYear}`, JSON.stringify(savedWorkDays)); renderReportTable(); } function updateReportTotals() { let totalWorkDays = 0; let totalWindows = 0; let totalPrice = 0; let monthsWithWorkDays = 0; reportData.forEach(month => { const workDays = parseInt(month.workDays) || 0; if (workDays > 0) { totalWorkDays += workDays; monthsWithWorkDays++; } totalWindows += month.windows; totalPrice += month.price; }); document.getElementById('totalWorkDays').textContent = totalWorkDays > 0 ? totalWorkDays : '-'; document.getElementById('totalWindows').textContent = totalWindows; document.getElementById('totalPrice').textContent = totalPrice.toFixed(2); // Calculate overall average per day if (totalWorkDays > 0) { const avgPerDay = (totalWindows / totalWorkDays).toFixed(2); document.getElementById('avgPerDay').textContent = avgPerDay; } else { document.getElementById('avgPerDay').textContent = '-'; } } function generateReportData() { // Report data is already in reportData array, ready for CSV generation console.log('Report data ready for CSV:', reportData); } function downloadCSV() { let csv = ''; let filename = ''; if (reportType === 'master') { // Master report CSV csv = 'Kuu,Tööpäevad,Aknad (Kogus),Summa (EUR),Keskmine päevas\n'; let totalWorkDays = 0; let totalWindows = 0; let totalPrice = 0; reportData.forEach(month => { const workDays = month.workDays || ''; const avgPerDay = month.avgPerDay > 0 ? month.avgPerDay.toFixed(2) : ''; csv += `${month.monthName},${workDays},${month.windows},${month.price.toFixed(2)},${avgPerDay}\n`; totalWorkDays += parseInt(month.workDays) || 0; totalWindows += month.windows; totalPrice += month.price; }); // Add totals row const totalAvg = totalWorkDays > 0 ? (totalWindows / totalWorkDays).toFixed(2) : ''; csv += `\nKOKKU,${totalWorkDays > 0 ? totalWorkDays : ''},${totalWindows},${totalPrice.toFixed(2)},${totalAvg}\n`; filename = `meistri_aruanne_${reportYear}.csv`; } else { // Accountant report CSV csv = 'KLIENT,Pakkum. Nr,Töö Nr,Kogus,Hind (EUR),Arve Nr\n'; let totalQuantity = 0; let totalPrice = 0; accountantReportData.forEach(record => { const client = (record.client_name || '-').replace(/,/g, ';'); const offer = (record.offer_number || '-').replace(/,/g, ';'); const work = (record.work_number || '-').replace(/,/g, ';'); const quantity = record.quantity || 0; const price = record.price ? record.price.toFixed(2) : '0.00'; const arveMakstud = (record.arve_makstud || '-').replace(/,/g, ';'); csv += `${client},${offer},${work},${quantity},${price},${arveMakstud}\n`; totalQuantity += parseInt(quantity) || 0; totalPrice += parseFloat(record.price) || 0; }); // Add totals row csv += `\nKOKKU,,,${totalQuantity},${totalPrice.toFixed(2)},\n`; filename = `raamatupidaja_aruanne_${reportYear}_${String(reportMonth).padStart(2, '0')}.csv`; } // Create blob and download const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // Load accountant report data (detailed client list) async function loadAccountantReportData() { try { const headers = token ? { Authorization: `Bearer ${token}` } : {}; // Fetch records for selected month and year const response = await axios.get(`${API_BASE}/api/records`, { params: { month: reportMonth, year: reportYear }, headers }); const records = response.data; // Show ALL records for accountant report (no filtering) const invoicedRecords = records; // Save to global variable for CSV/print accountantReportData = invoicedRecords; // Render accountant table const tbody = document.getElementById('accountantReportTableBody'); tbody.innerHTML = invoicedRecords.map(record => ` ${record.client_name || '-'} ${record.offer_number || '-'} ${record.work_number || '-'} ${record.quantity || 0} ${record.price ? record.price.toFixed(2) : '0.00'} ${record.arve_makstud || '-'} `).join(''); // Calculate totals const totalQuantity = invoicedRecords.reduce((sum, r) => sum + (parseInt(r.quantity) || 0), 0); const totalPrice = invoicedRecords.reduce((sum, r) => sum + (parseFloat(r.price) || 0), 0); // Update footer document.getElementById('accTotalQuantity').textContent = totalQuantity; document.getElementById('accTotalPrice').textContent = totalPrice.toFixed(2); console.log('Accountant report data ready:', invoicedRecords); } catch (error) { console.error('Load accountant report error:', error); alert('Viga aruande laadimisel: ' + (error.response?.data?.error || error.message)); } } function printReport() { // Create print area if it doesn't exist let printArea = document.getElementById('printArea'); if (!printArea) { printArea = document.createElement('div'); printArea.id = 'printArea'; printArea.style.display = 'none'; document.body.appendChild(printArea); } let htmlContent = ''; if (reportType === 'master') { // Master report print let totalWorkDays = 0; let totalWindows = 0; let totalPrice = 0; reportData.forEach(month => { totalWorkDays += parseInt(month.workDays) || 0; totalWindows += month.windows; totalPrice += month.price; }); const totalAvg = totalWorkDays > 0 ? (totalWindows / totalWorkDays).toFixed(2) : '-'; let tableRows = ''; reportData.forEach(month => { const workDays = month.workDays || '-'; const avgPerDay = month.avgPerDay > 0 ? month.avgPerDay.toFixed(2) : '-'; tableRows += ` ${month.monthName} ${workDays} ${month.windows} ${month.price.toFixed(2)} ${avgPerDay} `; }); htmlContent = `

Meistri aruanne

Aasta: ${reportYear}

${tableRows}
Kuu Tööpäevad Aknad (Kogus) Summa (€) Keskm./päev
KOKKU: ${totalWorkDays > 0 ? totalWorkDays : '-'} ${totalWindows} ${totalPrice.toFixed(2)} ${totalAvg}

Genereeritud: ${new Date().toLocaleDateString('et-EE')} ${new Date().toLocaleTimeString('et-EE')}

`; } else { // Accountant report print let totalQuantity = 0; let totalPrice = 0; let tableRows = ''; accountantReportData.forEach(record => { totalQuantity += parseInt(record.quantity) || 0; totalPrice += parseFloat(record.price) || 0; tableRows += ` ${record.client_name || '-'} ${record.offer_number || '-'} ${record.work_number || '-'} ${record.quantity || 0} ${record.price ? record.price.toFixed(2) : '0.00'} ${record.arve_makstud || '-'} `; }); const monthName = MONTH_NAMES[reportMonth - 1]; htmlContent = `

Raamatupidaja aruanne

Periood: ${monthName} ${reportYear}

${tableRows}
KLIENT Pakkum. Nr Töö Nr Kogus Hind (€) Arve Nr
KOKKU: ${totalQuantity} ${totalPrice.toFixed(2)}

Genereeritud: ${new Date().toLocaleDateString('et-EE')} ${new Date().toLocaleTimeString('et-EE')}

`; } printArea.innerHTML = htmlContent; printArea.style.display = 'block'; window.print(); printArea.style.display = 'none'; } // Handle delete permission checkbox document.getElementById('allowDeleteCheckbox').addEventListener('change', function() { allowDelete = this.checked; localStorage.setItem('allowDelete', allowDelete); toggleDeleteButtons(); }); // Toggle visibility of delete buttons function toggleDeleteButtons() { // Show delete buttons for all users const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = 'inline-block'; }); } // Confirm delete dialog function confirmDelete(recordId) { if (!token || !currentUser || currentUser.role !== 'admin') { alert('Ainult administraator saab kustutada kirjeid.'); return; } const record = currentRecords.find(r => r.id === recordId); if (!record) return; const confirmMessage = `Kas oled kindel, et soovid kustutada kirje?\n\nKlient: ${record.client_name}\nPakkumine: ${record.offer_number}\nTöö: ${record.work_number}\n\nKirje märgitakse kustutatuks ja eemaldatakse tabelist.`; if (confirm(confirmMessage)) { deleteRecord(recordId); } } // Delete record (soft delete) async function deleteRecord(recordId) { if (!recordId) return; try { await axios.delete( `${API_BASE}/api/records/${recordId}`, { headers: { Authorization: `Bearer ${token}` } } ); await loadRecords(); alert('Kirje on edukalt kustutatud.'); } catch (error) { console.error('Delete error:', error); if (error.response?.status === 401) { alert('Sessioon on aegunud. Palun logi uuesti sisse.'); localStorage.removeItem('token'); localStorage.removeItem('user'); token = null; currentUser = null; location.reload(); } else { alert('Viga kustutamisel: ' + (error.response?.data?.error || error.message)); } } }